Merge pull request #1 from StevanFreeborn/stevanfreeborn/feat/add-console-app
feat: add console app
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: Pull Request
|
||||
name: API Pull Request
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src
|
||||
@@ -23,8 +23,10 @@ jobs:
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x.x
|
||||
- name: Format project
|
||||
run: dotnet format --verbosity normal
|
||||
- name: Format projects
|
||||
run: |
|
||||
dotnet format ./AltGen.API/AltGen.API.csproj --verbosity normal
|
||||
dotnet format ./AltGen.API.Tests/AltGen.API.Tests.csproj --verbosity normal
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
@@ -47,10 +49,18 @@ jobs:
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x.x
|
||||
- name: Install report generator
|
||||
run: dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
run: dotnet restore ./AltGen.API.Tests/AltGen.API.Tests.csproj
|
||||
- name: Build project
|
||||
run: dotnet build --no-restore
|
||||
run: dotnet build ./AltGen.API.Tests/AltGen.API.Tests.csproj --no-restore
|
||||
- name: Test project
|
||||
run: dotnet test --filter FullyQualifiedName!~PromptEvaluation
|
||||
|
||||
env:
|
||||
Gemini__ApiKey: ${{ secrets.GEMINI_APIKEY }}
|
||||
run: dotnet test ./AltGen.API.Tests/AltGen.API.Tests.csproj --filter FullyQualifiedName!~PromptEvaluation
|
||||
- name: Upload test coverage report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-coverage
|
||||
path: ./AltGen.API.Tests/TestResults
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Console Pull Request
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- src/AltGen.Console/**
|
||||
- src/AltGen.Console.Tests/**
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
format:
|
||||
name: Run dotnet format
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x.x
|
||||
- name: Format projects
|
||||
run: |
|
||||
dotnet format ./AltGen.Console/AltGen.Console.csproj --verbosity normal
|
||||
dotnet format ./AltGen.Console.Tests/AltGen.Console.Tests.csproj --verbosity normal
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "<>"
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
git add .
|
||||
git commit -m "chore: format fixes [skip ci]"
|
||||
git fetch origin
|
||||
git pull --rebase origin ${{ github.head_ref }}
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
fi
|
||||
build_and_test:
|
||||
name: Run tests
|
||||
needs: format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x.x
|
||||
- name: Install report generator
|
||||
run: dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore ./AltGen.Console.Tests/AltGen.Console.Tests.csproj
|
||||
- name: Build project
|
||||
run: dotnet build ./AltGen.Console.Tests/AltGen.Console.Tests.csproj --no-restore
|
||||
- name: Test project
|
||||
run: dotnet test ./AltGen.Console.Tests/AltGen.Console.Tests.csproj
|
||||
- name: Upload test coverage report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-coverage
|
||||
path: ./AltGen.Console.Tests/TestResults
|
||||
@@ -1,16 +1,12 @@
|
||||
name: Deploy
|
||||
name: Deploy API
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '.gitignore'
|
||||
- '.editorconfig'
|
||||
- 'LICENSE.md'
|
||||
- '**/*/README.md'
|
||||
- '**/*/Dockerfile'
|
||||
paths:
|
||||
- src/AltGen.API/**
|
||||
- src/AltGen.API.Tests/**
|
||||
jobs:
|
||||
build:
|
||||
name: Build and push Docker image
|
||||
@@ -51,5 +47,5 @@ jobs:
|
||||
docker stop api.altgen.stevanfreeborn.com
|
||||
docker rm api.altgen.stevanfreeborn.com
|
||||
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ needs.build.outputs.version }}
|
||||
docker run -d -p 7778:8080 --name api.altgen.stevanfreeborn.com ${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ needs.build.outputs.version }}
|
||||
docker run --restart always -d -p 7778:8080 --name api.altgen.stevanfreeborn.com ${{ secrets.DOCKERHUB_USERNAME }}/api.altgen.stevanfreeborn.com:${{ needs.build.outputs.version }}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Publish Console
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- src/AltGen.Console/**
|
||||
- src/AltGen.Console.Tests/**
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.ACTIONS_PAT }}
|
||||
- name: Setup .NET 9
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x
|
||||
- name: Install versionize
|
||||
run: dotnet tool install --global Versionize
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "65925598+StevanFreeborn@users.noreply.github.com"
|
||||
git config --local user.name "Stevan Freeborn"
|
||||
- name: Run versionize
|
||||
id: versionize
|
||||
run: versionize -i --exit-insignificant-commits --workingDir ./src/AltGen.Console --commit-suffix "[skip ci]"
|
||||
continue-on-error: true
|
||||
- name: Upload changelog
|
||||
if: steps.versionize.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: change-log
|
||||
path: src/BGR.Console/CHANGELOG.md
|
||||
- name: Push changes to GitHub
|
||||
if: steps.versionize.outcome == 'success'
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.ACTIONS_PAT }}
|
||||
branch: ${{ github.ref }}
|
||||
tags: true
|
||||
outputs:
|
||||
is_new_version: ${{ steps.versionize.outcome == 'success' }}
|
||||
publish:
|
||||
needs: [version]
|
||||
if: needs.version.outputs.is_new_version == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.ACTIONS_PAT }}
|
||||
- name: Setup .NET 9
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x
|
||||
- name: Build
|
||||
run: dotnet build src
|
||||
- name: Publish for mac-os
|
||||
run: dotnet publish src/AltGen.Console/AltGen.Console.csproj -c Release -r osx-x64 --self-contained -o dist/mac-os -p:AltGenApiUri=${{ secrets.ALTGEN_API_URI }}
|
||||
- name: Publish for linux-os
|
||||
run: dotnet publish src/AltGen.Console/AltGen.Console.csproj -c Release -r linux-x64 --self-contained -o dist/linux-os -p:AltGenApiUri=${{ secrets.ALTGEN_API_URI }}
|
||||
- name: Publish for windows-os
|
||||
run: dotnet publish src/AltGen.Console/AltGen.Console.csproj -c Release -r win-x64 --self-contained -o dist/windows-os -p:AltGenApiUri=${{ secrets.ALTGEN_API_URI }}
|
||||
- name: Get project version
|
||||
uses: kzrnm/get-net-sdk-project-versions-action@v1
|
||||
id: get-version
|
||||
with:
|
||||
proj-path: src/AltGen.Console/AltGen.Console.csproj
|
||||
- name: Download changlog
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: change-log
|
||||
path: src/AltGen.Console
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_PAT }}
|
||||
name: bgr v${{ steps.get-version.outputs.version }}
|
||||
tag_name: v${{ steps.get-version.outputs.version }}
|
||||
draft: false
|
||||
body_path: src/AltGen.Console/CHANGELOG.md
|
||||
files: |
|
||||
dist/mac-os/AltGen.Console
|
||||
dist/linux-os/AltGen.Console
|
||||
dist/windows-os/AltGen.Console.exe
|
||||
@@ -7,6 +7,12 @@
|
||||
appsettings*.json
|
||||
!appsettings.Example.json
|
||||
|
||||
# generated
|
||||
src/AltGen.Console/Generated/**/*
|
||||
|
||||
# dist
|
||||
dist/**/*
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"inputs": [
|
||||
{
|
||||
"id": "arguments",
|
||||
"description": "The arguments to pass to the program",
|
||||
"type": "promptString",
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "AltGen.API",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build-api",
|
||||
"program": "${workspaceFolder}/src/AltGen.API/bin/Debug/net9.0/AltGen.API.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/AltGen.API",
|
||||
"stopAtEntry": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
},
|
||||
{
|
||||
"name": "AltGen.Console",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/src/AltGen.Console/bin/Debug/net9.0/AltGen.Console.dll",
|
||||
"args": "${input:arguments}",
|
||||
"cwd": "${workspaceFolder}/src/AltGen.Console",
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/AltGen.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/AltGen.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/AltGen.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</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.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
|
||||
@@ -49,7 +49,7 @@
|
||||
<Content Include="Files\**\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.Test.json">
|
||||
<Content Include="appsettings.Test.json" Condition="Exists('appsettings.Test.json')">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace AltGen.API.Tests.Fixtures;
|
||||
public class TestConfiguration
|
||||
{
|
||||
static IConfiguration Config { get; } = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.Test.json")
|
||||
.AddJsonFile("appsettings.Test.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ class AltTextProviderFactory(IServiceProvider serviceProvider) : IAltTextProvide
|
||||
|
||||
public IAltTextProvider Create(string provider)
|
||||
{
|
||||
return provider switch
|
||||
return provider.ToLowerInvariant() switch
|
||||
{
|
||||
LLMProvider.Gemini => _serviceProvider.GetRequiredKeyedService<IAltTextProvider>(LLMProvider.Gemini),
|
||||
_ => throw new NotSupportedException($"The provider '{provider}' is not supported.")
|
||||
|
||||
@@ -2,10 +2,10 @@ namespace AltGen.API.Generate.Providers;
|
||||
|
||||
static class LLMProvider
|
||||
{
|
||||
public const string Gemini = "Gemini";
|
||||
public const string Gemini = "gemini";
|
||||
|
||||
public static bool IsValid(string provider)
|
||||
{
|
||||
return provider is Gemini;
|
||||
return provider.ToLowerInvariant() is Gemini;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="Spectre.Console.Testing" Version="0.49.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="[7.2.0]" />
|
||||
</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>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AltGen.Console\AltGen.Console.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,118 @@
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class AddConfigCommandTests : IDisposable
|
||||
{
|
||||
readonly Mock<IAppSettingsManager> _mockSettingsManager = new();
|
||||
readonly TestConsole _testConsole = new();
|
||||
readonly AddConfigCommand _sut;
|
||||
|
||||
public AddConfigCommandTests()
|
||||
{
|
||||
_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()
|
||||
{
|
||||
var expectedAppSettings = new AppSettings([
|
||||
new("provider", "key", false)
|
||||
]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(false);
|
||||
|
||||
var commandSettings = new AddConfigCommand.Settings()
|
||||
{
|
||||
Provider = "provider",
|
||||
Key = "key",
|
||||
};
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!, commandSettings);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSettingsManager.Verify(
|
||||
static x => x.SaveAppSettingsAsync(It.Is<AppSettings>(
|
||||
static x => x.Providers[0].Provider == "provider" &&
|
||||
x.Providers[0].Key == "key" &&
|
||||
x.Providers[0].Default == false
|
||||
)),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSettingsExist_ItShouldUpdateSettings()
|
||||
{
|
||||
var existingAppSettings = new AppSettings([
|
||||
new("existing", "existing", false)
|
||||
]);
|
||||
|
||||
var expectedAppSettings = new AppSettings([
|
||||
new("existing", "key", true)
|
||||
]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(true);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.GetAppSettingsAsync())
|
||||
.ReturnsAsync(existingAppSettings);
|
||||
|
||||
var commandSettings = new AddConfigCommand.Settings()
|
||||
{
|
||||
Provider = "existing",
|
||||
Key = "key",
|
||||
Default = true,
|
||||
};
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!, commandSettings);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSettingsManager.Verify(
|
||||
static x => x.SaveAppSettingsAsync(It.Is<AppSettings>(
|
||||
static x => x.Providers[0].Provider == "existing" &&
|
||||
x.Providers[0].Key == "key" &&
|
||||
x.Providers[0].Default == true
|
||||
)),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_testConsole.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Net;
|
||||
|
||||
using FluentAssertions;
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class AltGenServiceTests : IDisposable
|
||||
{
|
||||
readonly MockHttpMessageHandler _mockHttpMessageHandler = new();
|
||||
readonly AltGenService _altGenService;
|
||||
|
||||
public AltGenServiceTests()
|
||||
{
|
||||
_altGenService = new AltGenService(_mockHttpMessageHandler.ToHttpClient());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAltTextAsync_WhenCalledAndRequestSucceeds_ItShouldReturnAltText()
|
||||
{
|
||||
var req = new GenerateAltTextRequest(
|
||||
"provider",
|
||||
"key",
|
||||
"file.jpg",
|
||||
[1, 2, 3],
|
||||
"image/jpeg"
|
||||
);
|
||||
|
||||
var altText = "alt text";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.When(HttpMethod.Post, "*/generate")
|
||||
.Respond(
|
||||
"application/json",
|
||||
/*lang=json,strict*/
|
||||
$@"{{""AltText"":""{altText}""}}"
|
||||
);
|
||||
|
||||
var result = await _altGenService.GenerateAltTextAsync(req);
|
||||
|
||||
result.Should().Be(altText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAltTextAsync_WhenCalledAndRequestFails_ItShouldThrowException()
|
||||
{
|
||||
var req = new GenerateAltTextRequest(
|
||||
"provider",
|
||||
"key",
|
||||
"file.jpg",
|
||||
[1, 2, 3],
|
||||
"image/jpeg"
|
||||
);
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.When(HttpMethod.Post, "*/generate")
|
||||
.Respond(HttpStatusCode.InternalServerError);
|
||||
|
||||
var action = async () => await _altGenService.GenerateAltTextAsync(req);
|
||||
|
||||
await action.Should().ThrowAsync<AltTextException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAltTextAsync_WhenCalledAndResponseIsNull_ItShouldThrowException()
|
||||
{
|
||||
var req = new GenerateAltTextRequest(
|
||||
"provider",
|
||||
"key",
|
||||
"file.jpg",
|
||||
[1, 2, 3],
|
||||
"image/jpeg"
|
||||
);
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.When(HttpMethod.Post, "*/generate")
|
||||
.Respond("application/json", "null");
|
||||
|
||||
var action = async () => await _altGenService.GenerateAltTextAsync(req);
|
||||
|
||||
await action.Should().ThrowAsync<AltTextException>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_mockHttpMessageHandler.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class AppSettingsManagerTests
|
||||
{
|
||||
readonly Mock<IFileSystem> _mockFileSystem = new();
|
||||
readonly AppSettingsManager _sut;
|
||||
|
||||
public AppSettingsManagerTests()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.Path.Combine(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns("path");
|
||||
|
||||
_sut = new AppSettingsManager(_mockFileSystem.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettingsExist_WhenSettingsDoNotExist_ItShouldReturnFalse()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.Path.Exists(It.IsAny<string>()))
|
||||
.Returns(false);
|
||||
|
||||
var result = _sut.AppSettingsExist();
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettingsExist_WhenSettingsExist_ItShouldReturnTrue()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.Path.Exists(It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
|
||||
var result = _sut.AppSettingsExist();
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppSettingsAsync_WhenSettingsExist_ItShouldReturnSettings()
|
||||
{
|
||||
var testSettings = new AppSettings([
|
||||
new("provider", "key", false)
|
||||
]);
|
||||
|
||||
var json = JsonSerializer.Serialize(testSettings, JsonOptions.Default);
|
||||
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync(json);
|
||||
|
||||
var result = await _sut.GetAppSettingsAsync();
|
||||
|
||||
result.Should().BeEquivalentTo(testSettings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppSettingsAsync_WhenDeserializingSettingsIsNull_ItShouldReturnEmptySettings()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ReturnsAsync("null");
|
||||
|
||||
var result = await _sut.GetAppSettingsAsync();
|
||||
|
||||
result.Should().BeEquivalentTo(new AppSettings([]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppSettingsAsync_WhenSettingsDoNotExist_ItShouldThrow()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.File.ReadAllTextAsync(It.IsAny<string>(), default))
|
||||
.ThrowsAsync(new FileNotFoundException());
|
||||
|
||||
var action = _sut.GetAppSettingsAsync;
|
||||
|
||||
await action.Should().ThrowAsync<FileNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAppSettingsAsync_WhenCalled_ItShouldSaveSettings()
|
||||
{
|
||||
_mockFileSystem
|
||||
.Setup(static x => x.File.WriteAllTextAsync(It.IsAny<string>(), It.IsAny<string>(), default))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var testSettings = new AppSettings([
|
||||
new("provider", "key", false)
|
||||
]);
|
||||
|
||||
var json = JsonSerializer.Serialize(testSettings, JsonOptions.Default);
|
||||
|
||||
await _sut.SaveAppSettingsAsync(testSettings);
|
||||
|
||||
_mockFileSystem.Verify(x => x.File.WriteAllTextAsync(It.IsAny<string>(), json, default), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class AppSettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddOrUpdateProvider_WhenProviderDoesNotExist_ItShouldAddProvider()
|
||||
{
|
||||
var providerSettings = new ProviderSettings("provider", "key", true);
|
||||
|
||||
var commandSettings = new AddConfigCommand.Settings()
|
||||
{
|
||||
Provider = providerSettings.Provider,
|
||||
Key = providerSettings.Key,
|
||||
Default = providerSettings.Default,
|
||||
};
|
||||
|
||||
var appSettings = new AppSettings([]);
|
||||
|
||||
var updatedAppSettings = appSettings.AddOrUpdateProvider(commandSettings);
|
||||
|
||||
updatedAppSettings.Should().BeEquivalentTo(new AppSettings([providerSettings]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdateProvider_WhenProviderDoesExist_ItShouldUpdateProvider()
|
||||
{
|
||||
var providerSettings = new ProviderSettings("provider", "key", true);
|
||||
var existingProviderSettings = new ProviderSettings("provider", "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]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrUpdateProvider_WhenExistingProviderIsSetToDefaultAndNewDefaultGiven_ItShouldUpdateProvidersCorrectly()
|
||||
{
|
||||
var providerSettings = new ProviderSettings("claude", "key", true);
|
||||
var existingProviderSettings = new ProviderSettings("gemini", "key", true);
|
||||
|
||||
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 with { Default = false }
|
||||
]));
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
var existingProviderSettings = new ProviderSettings("provider", "key", true);
|
||||
|
||||
var commandSettings = new RemoveConfigCommand.Settings()
|
||||
{
|
||||
Provider = existingProviderSettings.Provider,
|
||||
};
|
||||
|
||||
var appSettings = new AppSettings([existingProviderSettings]);
|
||||
|
||||
var updatedAppSettings = appSettings.RemoveProvider(commandSettings);
|
||||
|
||||
updatedAppSettings.Should().BeEquivalentTo(new AppSettings([]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
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,86 @@
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class ListConfigCommandTests : IDisposable
|
||||
{
|
||||
readonly Mock<IAppSettingsManager> _mockSettingsManager = new();
|
||||
readonly TestConsole _testConsole = new();
|
||||
readonly ListConfigCommand _sut;
|
||||
|
||||
public ListConfigCommandTests()
|
||||
{
|
||||
_sut = new ListConfigCommand(_testConsole, _mockSettingsManager.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldOutputNoSettings()
|
||||
{
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.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 testSettings = new AppSettings([
|
||||
new("provider", "key", false)
|
||||
]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(true);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.GetAppSettingsAsync())
|
||||
.ReturnsAsync(testSettings);
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_testConsole
|
||||
.Output
|
||||
.Should()
|
||||
.Contain("provider key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSettingsExistAndDefault_ItShouldOutputSettings()
|
||||
{
|
||||
var testSettings = new AppSettings([
|
||||
new("provider", "key", true)
|
||||
]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(true);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.GetAppSettingsAsync())
|
||||
.ReturnsAsync(testSettings);
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_testConsole
|
||||
.Output
|
||||
.Should()
|
||||
.Contain("provider key (default)");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_testConsole.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace AltGen.Console.Tests.Unit;
|
||||
|
||||
public class RemoveConfigCommandTests : IDisposable
|
||||
{
|
||||
readonly Mock<IAppSettingsManager> _mockSettingsManager = new();
|
||||
readonly TestConsole _testConsole = new();
|
||||
readonly RemoveConfigCommand _sut;
|
||||
|
||||
public RemoveConfigCommandTests()
|
||||
{
|
||||
_sut = new RemoveConfigCommand(_testConsole, _mockSettingsManager.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldThrow()
|
||||
{
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(false);
|
||||
|
||||
var commandSettings = new RemoveConfigCommand.Settings()
|
||||
{
|
||||
Provider = "provider",
|
||||
};
|
||||
|
||||
var action = async () => await _sut.ExecuteAsync(null!, commandSettings);
|
||||
|
||||
await action.Should().ThrowAsync<ConfigException>();
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenProviderDoesNotExist_ItShouldDoNothing()
|
||||
{
|
||||
var testSettings = new AppSettings([]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(true);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.GetAppSettingsAsync())
|
||||
.ReturnsAsync(testSettings);
|
||||
|
||||
var commandSettings = new RemoveConfigCommand.Settings()
|
||||
{
|
||||
Provider = "provider",
|
||||
};
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!, commandSettings);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSettingsManager.Verify(x => x.SaveAppSettingsAsync(testSettings), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenProviderExists_ItShouldRemoveProviderFromSettings()
|
||||
{
|
||||
var testSettings = new AppSettings([
|
||||
new("provider", "key", false)
|
||||
]);
|
||||
|
||||
var expectedSettings = new AppSettings([]);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.AppSettingsExist())
|
||||
.Returns(true);
|
||||
|
||||
_mockSettingsManager
|
||||
.Setup(static x => x.GetAppSettingsAsync())
|
||||
.ReturnsAsync(testSettings);
|
||||
|
||||
var commandSettings = new RemoveConfigCommand.Settings()
|
||||
{
|
||||
Provider = "provider",
|
||||
};
|
||||
|
||||
var result = await _sut.ExecuteAsync(null!, commandSettings);
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSettingsManager.Verify(x => x.SaveAppSettingsAsync(expectedSettings), Times.Once);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_testConsole.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
|
||||
global using Moq;
|
||||
|
||||
global using RichardSzalay.MockHttp;
|
||||
|
||||
global using Spectre.Console.Testing;
|
||||
@@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.1.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GeneratedCodeDirectory>Generated</GeneratedCodeDirectory>
|
||||
<GeneratedConstantsFile>$(GeneratedCodeDirectory)\Constants.cs</GeneratedConstantsFile>
|
||||
<AltGenApiUri>https://localhost:7297</AltGenApiUri>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GenerateBuildConstants" BeforeTargets="BeforeBuild;BeforeRebuild">
|
||||
|
||||
<MakeDir Directories="$(GeneratedCodeDirectory)" />
|
||||
|
||||
<WriteLinesToFile
|
||||
File="$(GeneratedConstantsFile)" Lines="
|
||||
namespace $(AssemblyName).$(GeneratedCodeDirectory)%3b
|
||||
|
||||
static class Constants
|
||||
{
|
||||
public const string AltGenApiUri = "$(AltGenApiUri)"%3b
|
||||
}
|
||||
"
|
||||
Overwrite="true" />
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(GeneratedConstantsFile)" Exclude="@(Compile)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace AltGen.Console.Common;
|
||||
|
||||
static class HostBuilderExtensions
|
||||
{
|
||||
public static CommandApp<GenerateCommand> BuildApp(this IHostBuilder builder)
|
||||
{
|
||||
var registrar = new TypeRegistrar(builder);
|
||||
var app = new CommandApp<GenerateCommand>(registrar);
|
||||
|
||||
app.Configure(static c =>
|
||||
{
|
||||
c.SetExceptionHandler(static (ex, resolver) =>
|
||||
{
|
||||
var console = resolver?.Resolve(typeof(IAnsiConsole)) as IAnsiConsole;
|
||||
console?.WriteException(ex, ExceptionFormats.ShortenEverything);
|
||||
return -99;
|
||||
});
|
||||
|
||||
c.AddBranch("config", static c =>
|
||||
{
|
||||
c.AddCommand<AddConfigCommand>("add");
|
||||
c.AddCommand<RemoveConfigCommand>("remove");
|
||||
c.AddCommand<ListConfigCommand>("list");
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AltGen.Console.Common;
|
||||
|
||||
static class JsonOptions
|
||||
{
|
||||
public static JsonSerializerOptions Default { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace AltGen.Console.Common;
|
||||
|
||||
static class Providers
|
||||
{
|
||||
const string Gemini = "gemini";
|
||||
|
||||
public static bool IsSupported(string provider)
|
||||
{
|
||||
return provider.ToLower(CultureInfo.InvariantCulture) switch
|
||||
{
|
||||
Gemini => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace AltGen.Console.Common;
|
||||
|
||||
sealed class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
|
||||
{
|
||||
readonly IHostBuilder _builder = builder;
|
||||
|
||||
public ITypeResolver Build()
|
||||
{
|
||||
return new TypeResolver(_builder.Build());
|
||||
}
|
||||
|
||||
public void Register(Type service, Type implementation)
|
||||
{
|
||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
|
||||
}
|
||||
|
||||
public void RegisterInstance(Type service, object implementation)
|
||||
{
|
||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
|
||||
}
|
||||
|
||||
public void RegisterLazy(Type service, Func<object> func)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(func);
|
||||
|
||||
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace AltGen.Console.Common;
|
||||
|
||||
sealed class TypeResolver(IHost host) : ITypeResolver, IDisposable
|
||||
{
|
||||
readonly IHost _host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
|
||||
public object? Resolve(Type? type)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _host.Services.GetService(type);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_host is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
sealed class AddConfigCommand(
|
||||
IAnsiConsole console,
|
||||
IAppSettingsManager settingsManager
|
||||
) : AsyncCommand<AddConfigCommand.Settings>
|
||||
{
|
||||
readonly IAnsiConsole _console = console;
|
||||
readonly IAppSettingsManager _settingsManager = settingsManager;
|
||||
|
||||
public class Settings : CommandSettings
|
||||
{
|
||||
[CommandArgument(1, "<provider>")]
|
||||
[Description("The provider to configure.")]
|
||||
public string Provider { get; init; } = string.Empty;
|
||||
|
||||
[CommandArgument(2, "<key>")]
|
||||
[Description("The key for the provider.")]
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
[CommandOption("-d|--default")]
|
||||
[Description("Set the provider as the default.")]
|
||||
public bool Default { get; init; }
|
||||
|
||||
public override ValidationResult Validate()
|
||||
{
|
||||
if (Providers.IsSupported(Provider) is false)
|
||||
{
|
||||
return ValidationResult.Error($"The provider '{Provider}' is not supported.");
|
||||
}
|
||||
|
||||
return ValidationResult.Success();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||
{
|
||||
if (_settingsManager.AppSettingsExist() is false)
|
||||
{
|
||||
var appSettings = new AppSettings([
|
||||
new(settings.Provider, settings.Key, settings.Default)
|
||||
]);
|
||||
await _settingsManager.SaveAppSettingsAsync(appSettings);
|
||||
_console.MarkupLine($"[bold]{settings.Provider}[/] has been configured.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var existingAppSettings = await _settingsManager.GetAppSettingsAsync();
|
||||
var updatedAppSettings = existingAppSettings.AddOrUpdateProvider(settings);
|
||||
await _settingsManager.SaveAppSettingsAsync(updatedAppSettings);
|
||||
_console.MarkupLine($"[bold]{settings.Provider}[/] has been configured.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
record AppSettings
|
||||
{
|
||||
public const string SettingsFileName = "appsettings.json";
|
||||
|
||||
public ProviderSettings[] Providers { get; init; } = [];
|
||||
|
||||
public AppSettings(ProviderSettings[] providers)
|
||||
{
|
||||
Providers = providers;
|
||||
}
|
||||
|
||||
public AppSettings AddOrUpdateProvider(AddConfigCommand.Settings settings)
|
||||
{
|
||||
var updatedProviders = Providers.Select(p =>
|
||||
{
|
||||
if (p.Provider == settings.Provider)
|
||||
{
|
||||
return p with { Key = settings.Key, Default = settings.Default };
|
||||
}
|
||||
|
||||
if (p.Provider != settings.Provider && settings.Default)
|
||||
{
|
||||
return p with { Default = false };
|
||||
}
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
var provider = Providers.FirstOrDefault(p => p.Provider == settings.Provider);
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
var newProvider = new ProviderSettings(settings.Provider, settings.Key, settings.Default);
|
||||
return this with { Providers = [.. updatedProviders, newProvider] };
|
||||
}
|
||||
|
||||
return this with { Providers = [.. updatedProviders] };
|
||||
}
|
||||
|
||||
public AppSettings RemoveProvider(RemoveConfigCommand.Settings settings)
|
||||
{
|
||||
var updatedProviders = Providers.Where(p => p.Provider != settings.Provider);
|
||||
return this with { Providers = [.. updatedProviders] };
|
||||
}
|
||||
|
||||
public ProviderSettings[] GetProviders()
|
||||
{
|
||||
return Providers;
|
||||
}
|
||||
|
||||
public ProviderSettings? GetProvider(string provider)
|
||||
{
|
||||
return Providers.FirstOrDefault(p => p.Provider == provider);
|
||||
}
|
||||
|
||||
public ProviderSettings? GetDefaultProvider()
|
||||
{
|
||||
return Providers.FirstOrDefault(static p => p.Default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
class AppSettingsManager(IFileSystem fileSystem) : IAppSettingsManager
|
||||
{
|
||||
const string SettingsFileName = "appsettings.json";
|
||||
readonly IFileSystem _fileSystem = fileSystem;
|
||||
|
||||
public bool AppSettingsExist()
|
||||
{
|
||||
return _fileSystem.Path.Exists(SettingsFileName);
|
||||
}
|
||||
|
||||
public async Task<AppSettings> GetAppSettingsAsync()
|
||||
{
|
||||
var existingJson = await _fileSystem.File.ReadAllTextAsync(GetSettingsPath());
|
||||
var existingAppSettings = JsonSerializer.Deserialize<AppSettings>(existingJson, JsonOptions.Default) ?? new AppSettings([]);
|
||||
return existingAppSettings;
|
||||
}
|
||||
|
||||
public async Task SaveAppSettingsAsync(AppSettings appSettings)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(appSettings, JsonOptions.Default);
|
||||
await _fileSystem.File.WriteAllTextAsync(GetSettingsPath(), json);
|
||||
}
|
||||
|
||||
string GetSettingsPath()
|
||||
{
|
||||
return _fileSystem.Path.Combine(AppContext.BaseDirectory, SettingsFileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
class ConfigException(string message) : Exception(message)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
interface IAppSettingsManager
|
||||
{
|
||||
bool AppSettingsExist();
|
||||
Task<AppSettings> GetAppSettingsAsync();
|
||||
Task SaveAppSettingsAsync(AppSettings appSettings);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
sealed class ListConfigCommand(
|
||||
IAnsiConsole console,
|
||||
IAppSettingsManager settingsManager
|
||||
) : AsyncCommand
|
||||
{
|
||||
readonly IAnsiConsole _console = console;
|
||||
readonly IAppSettingsManager _settingsManager = settingsManager;
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context)
|
||||
{
|
||||
if (_settingsManager.AppSettingsExist() is false)
|
||||
{
|
||||
_console.MarkupLine("No settings found.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var appSettings = await _settingsManager.GetAppSettingsAsync();
|
||||
|
||||
foreach (var provider in appSettings.GetProviders())
|
||||
{
|
||||
var providerName = provider.Provider;
|
||||
var providerKey = provider.Key;
|
||||
var isDefault = provider.Default ? " (default)" : string.Empty;
|
||||
_console.MarkupLine($"[bold]{providerName}[/] [dim]{providerKey}[/]{isDefault}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
record ProviderSettings(string Provider, string Key, bool Default);
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace AltGen.Console.Config;
|
||||
|
||||
sealed class RemoveConfigCommand(
|
||||
IAnsiConsole console,
|
||||
IAppSettingsManager settingsManager
|
||||
) : AsyncCommand<RemoveConfigCommand.Settings>
|
||||
{
|
||||
readonly IAnsiConsole _console = console;
|
||||
readonly IAppSettingsManager _settingsManager = settingsManager;
|
||||
|
||||
public class Settings : CommandSettings
|
||||
{
|
||||
[CommandArgument(1, "<provider>")]
|
||||
[Description("The provider to remove.")]
|
||||
public string Provider { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||
{
|
||||
if (_settingsManager.AppSettingsExist() is false)
|
||||
{
|
||||
throw new ConfigException("No existing settings found.");
|
||||
}
|
||||
|
||||
var appSettings = await _settingsManager.GetAppSettingsAsync();
|
||||
var updatedAppSettings = appSettings.RemoveProvider(settings);
|
||||
await _settingsManager.SaveAppSettingsAsync(updatedAppSettings);
|
||||
_console.MarkupLine($"[bold]{settings.Provider}[/] has been removed.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace AltGen.Console.Generate;
|
||||
|
||||
sealed class AltGenService(HttpClient client) : IAltGenService
|
||||
{
|
||||
static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
readonly HttpClient _client = client;
|
||||
|
||||
public async Task<string> GenerateAltTextAsync(GenerateAltTextRequest req)
|
||||
{
|
||||
var byteContent = new ByteArrayContent(req.Image);
|
||||
byteContent.Headers.ContentType = new MediaTypeHeaderValue(req.ContentType);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{Constants.AltGenApiUri}/generate")
|
||||
{
|
||||
Content = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(req.Provider), "provider" },
|
||||
{ new StringContent(req.ProviderKey), "providerKey" },
|
||||
{ byteContent, "file", req.FileName }
|
||||
}
|
||||
};
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
throw new AltTextException("Failed to generate alt text.");
|
||||
}
|
||||
|
||||
var altTextResponse = JsonSerializer.Deserialize<AltTextResponse>(content, JsonOptions) ?? throw new AltTextException("Failed to deserialize response.");
|
||||
|
||||
return altTextResponse.AltText;
|
||||
}
|
||||
}
|
||||
|
||||
record AltTextResponse(string AltText);
|
||||
|
||||
class AltTextException(string message) : Exception(message)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AltGen.Console.Generate;
|
||||
|
||||
record GenerateAltTextRequest(
|
||||
string Provider,
|
||||
string ProviderKey,
|
||||
string FileName,
|
||||
byte[] Image,
|
||||
string ContentType
|
||||
);
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace AltGen.Console.Generate;
|
||||
|
||||
sealed class GenerateCommand(
|
||||
IAnsiConsole console,
|
||||
IFileSystem fileSystem,
|
||||
IAltGenService altGenService,
|
||||
IAppSettingsManager settingsManager
|
||||
) : AsyncCommand<GenerateCommand.Settings>
|
||||
{
|
||||
|
||||
readonly IAnsiConsole _console = console;
|
||||
readonly IFileSystem _fileSystem = fileSystem;
|
||||
readonly IAltGenService _altGenService = altGenService;
|
||||
readonly IAppSettingsManager _settingsManager = settingsManager;
|
||||
|
||||
public class Settings(IFileSystem fileSystem) : CommandSettings
|
||||
{
|
||||
readonly Dictionary<string, string> _imageTypes = new()
|
||||
{
|
||||
[".jpeg"] = "image/jpeg",
|
||||
[".jpg"] = "image/jpeg",
|
||||
[".png"] = "image/png"
|
||||
};
|
||||
|
||||
readonly IFileSystem _fileSystem = fileSystem;
|
||||
|
||||
[CommandOption("-p|--provider")]
|
||||
[Description("The provider to use for generating alt text.")]
|
||||
public string Provider { get; init; } = string.Empty;
|
||||
|
||||
[CommandOption("-k|--key")]
|
||||
[Description("The key for the provider.")]
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
[CommandArgument(1, "<path>")]
|
||||
[Description("The path to the image to generate alt text for.")]
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public string ContentType => _imageTypes[_fileSystem.Path.GetExtension(Path)];
|
||||
|
||||
public override ValidationResult Validate()
|
||||
{
|
||||
var pathExists = _fileSystem.File.Exists(Path);
|
||||
|
||||
if (pathExists is false)
|
||||
{
|
||||
return ValidationResult.Error($"The path '{Path}' does not exist.");
|
||||
}
|
||||
|
||||
var extension = _fileSystem.Path.GetExtension(Path);
|
||||
|
||||
if (_imageTypes.ContainsKey(extension) is false)
|
||||
{
|
||||
return ValidationResult.Error($"The file '{Path}' is not a valid image file.");
|
||||
}
|
||||
|
||||
return ValidationResult.Success();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||
{
|
||||
var appSettings = await _settingsManager.GetAppSettingsAsync();
|
||||
var provider = settings.Provider;
|
||||
var key = settings.Key;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider))
|
||||
{
|
||||
var defaultProvider = appSettings.GetDefaultProvider()
|
||||
?? throw new AltTextException("Please specify a provider or set a default provider.");
|
||||
provider = defaultProvider.Provider;
|
||||
}
|
||||
|
||||
if (Providers.IsSupported(provider) is false)
|
||||
{
|
||||
throw new AltTextException($"The provider '{provider}' is not supported.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
var selectedProvider = appSettings.GetProvider(provider)
|
||||
?? throw new AltTextException("Please specify a key or set a default provider.");
|
||||
key = selectedProvider.Key;
|
||||
}
|
||||
|
||||
|
||||
var fileName = _fileSystem.Path.GetFileName(settings.Path);
|
||||
var image = await _fileSystem.File.ReadAllBytesAsync(settings.Path);
|
||||
|
||||
var altText = await _altGenService.GenerateAltTextAsync(new(
|
||||
provider,
|
||||
key,
|
||||
fileName,
|
||||
image,
|
||||
settings.ContentType
|
||||
));
|
||||
|
||||
_console.MarkupLine($"[bold]{altText}[/]");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
interface IAltGenService
|
||||
{
|
||||
Task<string> GenerateAltTextAsync(GenerateAltTextRequest req);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AltGen.Console.Generated;
|
||||
|
||||
static class Constants
|
||||
{
|
||||
public const string AltGenApiUri = "https://localhost:7297";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(static l => l.ClearProviders())
|
||||
.ConfigureServices(static (_, services) =>
|
||||
{
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IAppSettingsManager, AppSettingsManager>();
|
||||
services.AddSingleton(AnsiConsole.Console);
|
||||
services.AddHttpClient<IAltGenService, AltGenService>()
|
||||
.AddStandardResilienceHandler();
|
||||
})
|
||||
.BuildApp()
|
||||
.RunAsync(args);
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
|
||||
global using AltGen.Console.Common;
|
||||
global using AltGen.Console.Config;
|
||||
global using AltGen.Console.Generate;
|
||||
global using AltGen.Console.Generated;
|
||||
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
|
||||
global using Spectre.Console;
|
||||
global using Spectre.Console.Cli;
|
||||
@@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.API", "AltGen.API\Al
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.API.Tests", "AltGen.API.Tests\AltGen.API.Tests.csproj", "{FC95942B-3579-4F83-A447-A96D8EEF8E45}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.Console", "AltGen.Console\AltGen.Console.csproj", "{83DA683F-A2C5-4AB7-A6FD-294DFBC08D87}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.Console.Tests", "AltGen.Console.Tests\AltGen.Console.Tests.csproj", "{F7DC6A70-907B-448A-A69D-4E2E80FA3FD3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -24,5 +28,13 @@ Global
|
||||
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{83DA683F-A2C5-4AB7-A6FD-294DFBC08D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{83DA683F-A2C5-4AB7-A6FD-294DFBC08D87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{83DA683F-A2C5-4AB7-A6FD-294DFBC08D87}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{83DA683F-A2C5-4AB7-A6FD-294DFBC08D87}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7DC6A70-907B-448A-A69D-4E2E80FA3FD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F7DC6A70-907B-448A-A69D-4E2E80FA3FD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F7DC6A70-907B-448A-A69D-4E2E80FA3FD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7DC6A70-907B-448A-A69D-4E2E80FA3FD3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Reference in New Issue
Block a user