Merge pull request 'feat: add secure config CLI' (#2) from stevanfreeborn/feat/add-secure-config-cli into main
This commit is contained in:
@@ -90,6 +90,7 @@ dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
#### C# Coding Conventions ####
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.IDE0053.severity = none
|
||||
dotnet_diagnostic.IDE0058.severity = none
|
||||
dotnet_diagnostic.IDE0100.severity = none
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# appsettings
|
||||
appsettings*.json
|
||||
!appsettings.Example.json
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/StevanFreeborn.Extensions.Configuration.Secure.Cli/StevanFreeborn.Extensions.Configuration.Secure.Cli.csproj" />
|
||||
<Project Path="src/StevanFreeborn.Extensions.Configuration.Secure.Sample/StevanFreeborn.Extensions.Configuration.Secure.Sample.csproj" />
|
||||
<Project Path="src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.csproj" />
|
||||
<Project Path="tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1303.severity = none;
|
||||
dotnet_diagnostic.CA2007.severity = none;
|
||||
dotnet_diagnostic.CA1822.severity = none;
|
||||
dotnet_diagnostic.CA1812.severity = none;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class DeleteCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "delete";
|
||||
private const string CommandDescription = "Delete the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to remove."
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var isDeleted = await _secureConfig.DeleteAsync(key, ct);
|
||||
|
||||
if (isDeleted)
|
||||
{
|
||||
_terminal.WriteLine($"Successfully removed value for {key}");
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
_terminal.WriteLine($"Unable to remove value for {key}");
|
||||
return ExitCodes.KeyNotRemoved;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal static class ExitCodes
|
||||
{
|
||||
public const int Success = 0;
|
||||
public const int KeyNotFound = 1;
|
||||
public const int KeyNotRemoved = 2;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class GetCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "get";
|
||||
private const string CommandDescription = "Retrieves the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to retrieve."
|
||||
};
|
||||
|
||||
private readonly Option<bool> _prettyOption = new("--pretty", "-p")
|
||||
{
|
||||
Description = "Indicates whether the value - if JSON - should be pretty printed or not"
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
_prettyOption
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var shouldPrintPretty = result.GetValue(_prettyOption);
|
||||
var value = await _secureConfig.GetAsync<string>(key, ct);
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
_terminal.WriteLine($"Unable to retrieve value for {key}");
|
||||
return ExitCodes.KeyNotFound;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(value);
|
||||
|
||||
if (shouldPrintPretty)
|
||||
{
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
var context = new CliJsonContext(options);
|
||||
value = JsonSerializer.Serialize(json.RootElement, context.JsonElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = JsonSerializer.Serialize(json.RootElement, CliJsonContext.Default.JsonElement);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
_terminal.WriteLine(value);
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.CommandLine;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal interface ICommandFactory
|
||||
{
|
||||
Command Create();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal interface ITerminal
|
||||
{
|
||||
public void WriteLine(string value);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class SetCommandFactory(
|
||||
ISecureConfig secureConfig,
|
||||
ITerminal terminal
|
||||
) : ICommandFactory
|
||||
{
|
||||
private const string CommandName = "set";
|
||||
private const string CommandDescription = "Sets the value for the given key";
|
||||
|
||||
private readonly Argument<string> _keyArg = new("key")
|
||||
{
|
||||
Description = "The key whose value you want to set."
|
||||
};
|
||||
|
||||
private readonly Argument<string> _valueArg = new("value")
|
||||
{
|
||||
Description = "The value that you want to set for the key."
|
||||
};
|
||||
|
||||
private readonly ISecureConfig _secureConfig = secureConfig;
|
||||
private readonly ITerminal _terminal = terminal;
|
||||
|
||||
public Command Create()
|
||||
{
|
||||
var command = new Command(CommandName, CommandDescription)
|
||||
{
|
||||
_keyArg,
|
||||
_valueArg,
|
||||
};
|
||||
|
||||
command.SetAction(HandleAsync);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private async Task<int> HandleAsync(ParseResult result, CancellationToken ct)
|
||||
{
|
||||
var key = result.GetRequiredValue(_keyArg);
|
||||
var value = result.GetRequiredValue(_valueArg);
|
||||
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(value);
|
||||
value = JsonSerializer.Serialize(json.RootElement, CliJsonContext.Default.JsonElement);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
await _secureConfig.SetAsync(key, value, ct);
|
||||
|
||||
_terminal.WriteLine($"Successfully set value of {key}");
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
internal sealed class Terminal : ITerminal
|
||||
{
|
||||
public void WriteLine(string value)
|
||||
{
|
||||
Console.WriteLine(value);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class Base64KeyOptions
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class CryptoProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = CryptoProviderTypes.Aes;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class CryptoProviderTypes
|
||||
{
|
||||
public const string Aes = "aes";
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class JsonStorageOptions
|
||||
{
|
||||
public string DirectoryPath { get; set; } = AppContext.BaseDirectory;
|
||||
public string FileName { get; set; } = "secure_config.json";
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class KeyProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = KeyProviderTypes.MachineId;
|
||||
public Base64KeyOptions Base64 { get; set; } = new();
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class KeyProviderTypes
|
||||
{
|
||||
public const string MachineId = "machineid";
|
||||
public const string Base64 = "base64";
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class SecureConfigBuilderExtensions
|
||||
{
|
||||
public static ISecureConfigBuilder ApplyProfile(
|
||||
this ISecureConfigBuilder builder,
|
||||
SecureConfigProfile profile
|
||||
)
|
||||
{
|
||||
ConfigureStorage(builder, profile.Storage);
|
||||
ConfigureKey(builder, profile.Key);
|
||||
ConfigureCrypto(builder, profile.Crypto);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureStorage(ISecureConfigBuilder builder, StorageProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case StorageProviderTypes.Json:
|
||||
builder.UseJsonFileStorage(opt =>
|
||||
{
|
||||
opt.DirectoryPath = options.Json.DirectoryPath;
|
||||
opt.FileName = options.Json.FileName;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Storage provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureKey(ISecureConfigBuilder builder, KeyProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case KeyProviderTypes.MachineId:
|
||||
builder.WithMachineIdKey();
|
||||
break;
|
||||
case KeyProviderTypes.Base64:
|
||||
builder.WithBase64EncryptionKey(options.Base64.Key);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Key provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureCrypto(ISecureConfigBuilder builder, CryptoProviderOptions options)
|
||||
{
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
switch (options.Type.ToLowerInvariant())
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
{
|
||||
case CryptoProviderTypes.Aes:
|
||||
builder.WithAesCryptoProvider();
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Crypto provider type '{options.Type}' is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class SecureConfigCliOptions
|
||||
{
|
||||
public string DefaultProfile { get; set; } = "Default";
|
||||
public Dictionary<string, SecureConfigProfile> Profiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class SecureConfigProfile
|
||||
{
|
||||
public StorageProviderOptions Storage { get; set; } = new();
|
||||
public KeyProviderOptions Key { get; set; } = new();
|
||||
public CryptoProviderOptions Crypto { get; set; } = new();
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal sealed class StorageProviderOptions
|
||||
{
|
||||
public string Type { get; set; } = StorageProviderTypes.Json;
|
||||
public JsonStorageOptions Json { get; set; } = new();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
|
||||
internal static class StorageProviderTypes
|
||||
{
|
||||
public const string Json = "json";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class CliJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Json;
|
||||
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(static config =>
|
||||
{
|
||||
config.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", false);
|
||||
})
|
||||
.ConfigureLogging(static logging => logging.ClearProviders())
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.AddSingleton(args);
|
||||
|
||||
services.AddSecureConfig(b =>
|
||||
{
|
||||
var cliOptions = new SecureConfigCliOptions();
|
||||
ctx.Configuration.Bind(cliOptions);
|
||||
|
||||
var profileName = ctx.Configuration.GetValue<string>("profile") ?? cliOptions.DefaultProfile;
|
||||
|
||||
if (cliOptions.Profiles.TryGetValue(profileName, out var profile) is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Secure configuration profile '{profileName}' not found.");
|
||||
}
|
||||
|
||||
b.AddJsonAotContext(CliJsonContext.Default).ApplyProfile(profile);
|
||||
});
|
||||
|
||||
services.AddSingleton<ITerminal, Terminal>();
|
||||
services.AddSingleton<ICommandFactory, GetCommandFactory>();
|
||||
services.AddSingleton<ICommandFactory, SetCommandFactory>();
|
||||
services.AddSingleton<ICommandFactory, DeleteCommandFactory>();
|
||||
services.AddHostedService<SecureConfigCli>();
|
||||
})
|
||||
.RunConsoleAsync();
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli;
|
||||
|
||||
internal sealed class SecureConfigCli : IHostedService
|
||||
{
|
||||
private readonly string[] _args;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly RootCommand _rootCommand = new("secure-config is a command line utility used to manage your encrypted configuration")
|
||||
{
|
||||
new Option<string>("--profile")
|
||||
{
|
||||
Description = "Allows specifying which profile to use from the appsettings.json file.",
|
||||
},
|
||||
};
|
||||
|
||||
public SecureConfigCli(
|
||||
string[] args,
|
||||
IHostApplicationLifetime lifetime,
|
||||
IEnumerable<ICommandFactory> commandFactories
|
||||
)
|
||||
{
|
||||
_args = args;
|
||||
_lifetime = lifetime;
|
||||
|
||||
foreach (var factory in commandFactories)
|
||||
{
|
||||
_rootCommand.Add(factory.Create());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var exitCode = await _rootCommand.Parse(_args[1..]).InvokeAsync(cancellationToken: cancellationToken);
|
||||
Environment.ExitCode = exitCode;
|
||||
|
||||
_lifetime.StopApplication();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>secure-config</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(MSBuildProjectName).Tests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StevanFreeborn.Extensions.Configuration.Secure\StevanFreeborn.Extensions.Configuration.Secure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="appsettings.*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,12 +1,10 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure;
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AwesomeAssertions" Version="9.4.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild" Version="8.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
|
||||
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
|
||||
<Include>[*]StevanFreeborn.Extensions.Configuration.Secure.Cli*</Include>
|
||||
<ExcludeByFile>**/Program.cs</ExcludeByFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
|
||||
<Exec Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="AwesomeAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference
|
||||
Include="..\..\src\StevanFreeborn.Extensions.Configuration.Secure.Cli\StevanFreeborn.Extensions.Configuration.Secure.Cli.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class DeleteCommandFactoryTests
|
||||
{
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly DeleteCommandFactory _sut;
|
||||
|
||||
public DeleteCommandFactoryTests()
|
||||
{
|
||||
_sut = new(_mockSecureConfig.Object, _mockTerminal.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WhenCalled_ItShouldReturnExpectedCommand()
|
||||
{
|
||||
var expectedCommand = new Command("delete", "Delete the value for the given key")
|
||||
{
|
||||
new Argument<string>("key")
|
||||
{
|
||||
Description = "The key whose value you want to remove."
|
||||
},
|
||||
};
|
||||
|
||||
var result = _sut.Create();
|
||||
|
||||
result.Should().BeEquivalentTo(expectedCommand, static o => o.IgnoringCyclicReferences());
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class DeleteCommandTests
|
||||
{
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly Command _sut;
|
||||
|
||||
public DeleteCommandTests()
|
||||
{
|
||||
_sut = new DeleteCommandFactory(_mockSecureConfig.Object, _mockTerminal.Object).Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenCalledWithoutKey_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var result = await _sut.Parse([]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenCalledWithNonExistentKey_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
|
||||
_mockSecureConfig.Setup(m => m.DeleteAsync(expectedKey, It.IsAny<CancellationToken>())).ReturnsAsync(false);
|
||||
|
||||
var result = await _sut.Parse([expectedKey]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenCalledWithExistingKey_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
|
||||
_mockSecureConfig.Setup(m => m.DeleteAsync(expectedKey, It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
||||
|
||||
var result = await _sut.Parse([expectedKey]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class GetCommandFactoryTests
|
||||
{
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly GetCommandFactory _sut;
|
||||
|
||||
public GetCommandFactoryTests()
|
||||
{
|
||||
_sut = new(_mockSecureConfig.Object, _mockTerminal.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WhenCalled_ItShouldReturnExpectedCommand()
|
||||
{
|
||||
var expectedCommand = new Command("get", "Retrieves the value for the given key")
|
||||
{
|
||||
new Argument<string>("key")
|
||||
{
|
||||
Description = "The key whose value you want to retrieve."
|
||||
},
|
||||
new Option<bool>("--pretty", "-p")
|
||||
{
|
||||
Description = "Indicates whether the value - if JSON - should be pretty printed or not"
|
||||
},
|
||||
};
|
||||
|
||||
var result = _sut.Create();
|
||||
|
||||
result.Should().BeEquivalentTo(expectedCommand, static o => o.IgnoringCyclicReferences());
|
||||
result.Action.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class GetCommandTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly Command _sut;
|
||||
|
||||
public GetCommandTests()
|
||||
{
|
||||
_sut = new GetCommandFactory(_mockSecureConfig.Object, _mockTerminal.Object).Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledWithoutKeyArgument_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var result = await _sut.Parse([]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledWithNonExistentKey_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
|
||||
_mockSecureConfig
|
||||
.Setup(m => m.GetAsync<string>(expectedKey, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((string?)null);
|
||||
|
||||
var result = await _sut.Parse([expectedKey]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledWithKeyForNonJsonValue_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
var expectedValue = "value";
|
||||
|
||||
_mockSecureConfig
|
||||
.Setup(m => m.GetAsync<string>(expectedKey, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedValue);
|
||||
|
||||
var result = await _sut.Parse([expectedKey]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledWithKeyForJsonValue_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
var expectedValue = JsonSerializer.Serialize(new { ApiKey = "Key" });
|
||||
|
||||
_mockSecureConfig
|
||||
.Setup(m => m.GetAsync<string>(expectedKey, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedValue);
|
||||
|
||||
var result = await _sut.Parse([expectedKey]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenCalledWithKeyForJsonValueAndPrettyPrint_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
var testValue = new { ApiKey = "Key" };
|
||||
var expectedValue = JsonSerializer.Serialize(testValue);
|
||||
var expectedPrintedValue = JsonSerializer.Serialize(testValue, JsonOptions);
|
||||
|
||||
_mockSecureConfig
|
||||
.Setup(m => m.GetAsync<string>(expectedKey, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedValue);
|
||||
|
||||
var result = await _sut.Parse([expectedKey, "-p"]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockTerminal.Verify(m => m.WriteLine(expectedPrintedValue), Times.Once());
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class SetCommandFactoryTests
|
||||
{
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly SetCommandFactory _sut;
|
||||
|
||||
public SetCommandFactoryTests()
|
||||
{
|
||||
_sut = new(_mockSecureConfig.Object, _mockTerminal.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WhenCalled_ItShouldReturnExpectedCommand()
|
||||
{
|
||||
var expectedCommand = new Command("set", "Sets the value for the given key")
|
||||
{
|
||||
new Argument<string>("key")
|
||||
{
|
||||
Description = "The key whose value you want to set.",
|
||||
},
|
||||
new Argument<string>("value")
|
||||
{
|
||||
Description = "The value that you want to set for the key."
|
||||
},
|
||||
};
|
||||
|
||||
var result = _sut.Create();
|
||||
|
||||
result.Should().BeEquivalentTo(expectedCommand, static o => o.IgnoringCyclicReferences());
|
||||
result.Action.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
|
||||
using Moq;
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class SetCommandTests
|
||||
{
|
||||
private readonly Mock<ISecureConfig> _mockSecureConfig = new();
|
||||
private readonly Mock<ITerminal> _mockTerminal = new();
|
||||
private readonly Command _sut;
|
||||
|
||||
public SetCommandTests()
|
||||
{
|
||||
_sut = new SetCommandFactory(_mockSecureConfig.Object, _mockTerminal.Object).Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_WhenCalledWithoutKey_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var result = await _sut.Parse([]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_WhenCalledWithoutValue_ItShouldReturnNonZeroExitCode()
|
||||
{
|
||||
var result = await _sut.Parse(["key"]).InvokeAsync();
|
||||
|
||||
result.Should().NotBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_WhenCalledWithKeyAndValue_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
var expectedValue = "value";
|
||||
|
||||
var result = await _sut.Parse([expectedKey, expectedValue]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSecureConfig.Verify(m => m.SetAsync(expectedKey, expectedValue, It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Set_WhenCalledWithKeyAndJsonValue_ItShouldReturnZeroExitCode()
|
||||
{
|
||||
var expectedKey = "key";
|
||||
var expectedValue = JsonSerializer.Serialize(new { ApiKey = "ApiKey" });
|
||||
|
||||
var result = await _sut.Parse([expectedKey, expectedValue]).InvokeAsync();
|
||||
|
||||
result.Should().Be(0);
|
||||
|
||||
_mockSecureConfig.Verify(m => m.SetAsync(expectedKey, expectedValue, It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Commands;
|
||||
|
||||
public class TerminalTests : IDisposable
|
||||
{
|
||||
private readonly StringWriter _stringWriter = new();
|
||||
private readonly Terminal _sut = new();
|
||||
|
||||
public TerminalTests()
|
||||
{
|
||||
Console.SetOut(_stringWriter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteLine_WhenCalled_ItShouldWriteToConsole()
|
||||
{
|
||||
var message = "test message";
|
||||
|
||||
_sut.WriteLine(message);
|
||||
|
||||
_stringWriter.ToString().Should().Be($"test message{Environment.NewLine}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var standardOutput = new StreamWriter(Console.OpenStandardOutput())
|
||||
{
|
||||
AutoFlush = true
|
||||
};
|
||||
|
||||
Console.SetOut(standardOutput);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
using Moq;
|
||||
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Configuration;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit.Configuration;
|
||||
|
||||
public class SecureConfigBuilderExtensionsTests
|
||||
{
|
||||
private readonly Mock<ISecureConfigBuilder> _mockBuilder = new();
|
||||
|
||||
[Fact]
|
||||
public void ApplyProfile_WhenCalledWithValidOptions_ItShouldConfigureBuilder()
|
||||
{
|
||||
_mockBuilder.Setup(static m => m.UseJsonFileStorage(It.IsAny<Action<Storage.JsonStorageOptions>>()))
|
||||
.Callback<Action<Storage.JsonStorageOptions>>(static a => a(new Storage.JsonStorageOptions()))
|
||||
.Returns(_mockBuilder.Object);
|
||||
|
||||
_mockBuilder.Setup(static m => m.WithMachineIdKey()).Returns(_mockBuilder.Object);
|
||||
_mockBuilder.Setup(static m => m.WithBase64EncryptionKey(It.IsAny<string>())).Returns(_mockBuilder.Object);
|
||||
_mockBuilder.Setup(static m => m.WithAesCryptoProvider()).Returns(_mockBuilder.Object);
|
||||
|
||||
var profile = new SecureConfigProfile
|
||||
{
|
||||
Storage = new StorageProviderOptions
|
||||
{
|
||||
Type = "json",
|
||||
Json = new JsonStorageOptions { DirectoryPath = "dir", FileName = "file.json" }
|
||||
},
|
||||
Key = new KeyProviderOptions
|
||||
{
|
||||
Type = "machineid"
|
||||
},
|
||||
Crypto = new CryptoProviderOptions
|
||||
{
|
||||
Type = "aes"
|
||||
}
|
||||
};
|
||||
|
||||
var result = _mockBuilder.Object.ApplyProfile(profile);
|
||||
|
||||
result.Should().Be(_mockBuilder.Object);
|
||||
_mockBuilder.Verify(static m => m.UseJsonFileStorage(It.IsAny<Action<Storage.JsonStorageOptions>>()), Times.Once());
|
||||
_mockBuilder.Verify(static m => m.WithMachineIdKey(), Times.Once());
|
||||
_mockBuilder.Verify(static m => m.WithAesCryptoProvider(), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyProfile_WhenCalledWithBase64Key_ItShouldConfigureBuilder()
|
||||
{
|
||||
var builderMock = new Mock<ISecureConfigBuilder>();
|
||||
|
||||
var profile = new SecureConfigProfile
|
||||
{
|
||||
Storage = new StorageProviderOptions { Type = "json", Json = new Cli.Configuration.JsonStorageOptions() },
|
||||
Key = new KeyProviderOptions { Type = "base64", Base64 = new Base64KeyOptions { Key = "test-key" } },
|
||||
Crypto = new CryptoProviderOptions { Type = "aes" }
|
||||
};
|
||||
|
||||
builderMock.Object.ApplyProfile(profile);
|
||||
|
||||
builderMock.Verify(m => m.WithBase64EncryptionKey("test-key"), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyProfile_WhenUnsupportedStorage_ItShouldThrowNotSupportedException()
|
||||
{
|
||||
var builderMock = new Mock<ISecureConfigBuilder>();
|
||||
var profile = new SecureConfigProfile
|
||||
{
|
||||
Storage = new StorageProviderOptions { Type = "unsupported" },
|
||||
Key = new KeyProviderOptions { Type = "machineid" },
|
||||
Crypto = new CryptoProviderOptions { Type = "aes" }
|
||||
};
|
||||
|
||||
Action act = () => builderMock.Object.ApplyProfile(profile);
|
||||
|
||||
act.Should().Throw<NotSupportedException>().WithMessage("Storage provider type 'unsupported' is not supported.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyProfile_WhenUnsupportedKey_ItShouldThrowNotSupportedException()
|
||||
{
|
||||
var builderMock = new Mock<ISecureConfigBuilder>();
|
||||
var profile = new SecureConfigProfile
|
||||
{
|
||||
Storage = new StorageProviderOptions { Type = "json", Json = new Cli.Configuration.JsonStorageOptions() },
|
||||
Key = new KeyProviderOptions { Type = "unsupported" },
|
||||
Crypto = new CryptoProviderOptions { Type = "aes" }
|
||||
};
|
||||
|
||||
Action act = () => builderMock.Object.ApplyProfile(profile);
|
||||
|
||||
act.Should().Throw<NotSupportedException>().WithMessage("Key provider type 'unsupported' is not supported.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyProfile_WhenUnsupportedCrypto_ItShouldThrowNotSupportedException()
|
||||
{
|
||||
var builderMock = new Mock<ISecureConfigBuilder>();
|
||||
var profile = new SecureConfigProfile
|
||||
{
|
||||
Storage = new StorageProviderOptions { Type = "json", Json = new Cli.Configuration.JsonStorageOptions() },
|
||||
Key = new KeyProviderOptions { Type = "machineid" },
|
||||
Crypto = new CryptoProviderOptions { Type = "unsupported" }
|
||||
};
|
||||
|
||||
Action act = () => builderMock.Object.ApplyProfile(profile);
|
||||
|
||||
act.Should().Throw<NotSupportedException>().WithMessage("Crypto provider type 'unsupported' is not supported.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SecureConfigCliOptions_ShouldBeCovered()
|
||||
{
|
||||
var options = new SecureConfigCliOptions
|
||||
{
|
||||
DefaultProfile = "default",
|
||||
Profiles = []
|
||||
};
|
||||
options.DefaultProfile.Should().Be("default");
|
||||
options.Profiles.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
using System.CommandLine;
|
||||
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
|
||||
using Moq;
|
||||
|
||||
|
||||
using StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
|
||||
|
||||
namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.Unit;
|
||||
|
||||
public class SecureConfigCliTests
|
||||
{
|
||||
private readonly Mock<IHostApplicationLifetime> _mockLifetime = new();
|
||||
private readonly Mock<ICommandFactory> _mockCommandFactory = new();
|
||||
|
||||
public SecureConfigCliTests()
|
||||
{
|
||||
_mockCommandFactory.Setup(m => m.Create()).Returns(new Command("test-command"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldCallCreateOnAllFactoriesPassedIn()
|
||||
{
|
||||
var sut = new SecureConfigCli(["secure-config"], _mockLifetime.Object, [_mockCommandFactory.Object]);
|
||||
_mockCommandFactory.Verify(static m => m.Create(), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WhenCalled_ItShouldRunTheCLI()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new SecureConfigCli(["secure-config", "--help"], _mockLifetime.Object, [_mockCommandFactory.Object]);
|
||||
|
||||
// Act
|
||||
await sut.StartAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
_mockLifetime.Verify(m => m.StopApplication(), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsync_WhenCalled_ItShouldNotThrowException()
|
||||
{
|
||||
var sut = new SecureConfigCli(["secure-config"], _mockLifetime.Object, [_mockCommandFactory.Object]);
|
||||
var act = async () => await sut.StopAsync(CancellationToken.None);
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
}
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
+18
-2
@@ -13,8 +13,12 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="8.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
@@ -23,6 +27,18 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CollectCoverage>true</CollectCoverage>
|
||||
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
|
||||
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
|
||||
<Include>[StevanFreeborn.Extensions.Configuration.Secure]*</Include>
|
||||
<ExcludeByFile>**/Program.cs</ExcludeByFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
|
||||
<Exec Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="AwesomeAssertions" />
|
||||
|
||||
+2
-4
@@ -84,8 +84,7 @@ public class SecureConfigBuilderTests
|
||||
{
|
||||
var act = () => _sut.UseJsonFileStorage((Action<JsonStorageOptions>)null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("configure");
|
||||
act.Should().Throw<ArgumentNullException>().WithParameterName("configure");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -104,8 +103,7 @@ public class SecureConfigBuilderTests
|
||||
{
|
||||
var act = () => _sut.AddJsonAotContext(null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("context");
|
||||
act.Should().Throw<ArgumentNullException>().WithParameterName("context");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+4
-6
@@ -24,8 +24,7 @@ public class SecureConfigSourceTests
|
||||
loggerFactory: _mockLoggerFactory.Object
|
||||
);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("storageProvider");
|
||||
act.Should().Throw<ArgumentNullException>().WithParameterName("storageProvider");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -37,8 +36,7 @@ public class SecureConfigSourceTests
|
||||
loggerFactory: _mockLoggerFactory.Object
|
||||
);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("cryptoProvider");
|
||||
act.Should().Throw<ArgumentNullException>().WithParameterName("cryptoProvider");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -50,8 +48,7 @@ public class SecureConfigSourceTests
|
||||
loggerFactory: null!
|
||||
);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("loggerFactory");
|
||||
act.Should().Throw<ArgumentNullException>().WithParameterName("loggerFactory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -87,6 +84,7 @@ public class SecureConfigSourceTests
|
||||
public void Build_WhenCalled_ItShouldCreateLoggerFromFactory()
|
||||
{
|
||||
var mockLogger = new Mock<ILogger<SecureConfigProvider>>();
|
||||
|
||||
_mockLoggerFactory
|
||||
.Setup(f => f.CreateLogger(typeof(SecureConfigProvider).FullName!))
|
||||
.Returns(mockLogger.Object);
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user