diff --git a/.editorconfig b/.editorconfig
index 8296f0a..9ef75e6 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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
diff --git a/.gitignore b/.gitignore
index 3b15c69..4e204df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,10 @@
##
## Get latest from `dotnet new gitignore`
+# appsettings
+appsettings*.json
+!appsettings.Example.json
+
# dotenv files
.env
diff --git a/StevanFreeborn.SecureConfig.slnx b/StevanFreeborn.SecureConfig.slnx
index 27522a1..8513f10 100644
--- a/StevanFreeborn.SecureConfig.slnx
+++ b/StevanFreeborn.SecureConfig.slnx
@@ -1,9 +1,11 @@
+
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/.editorconfig b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/.editorconfig
new file mode 100644
index 0000000..acaa91a
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/.editorconfig
@@ -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;
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/DeleteCommandFactory.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/DeleteCommandFactory.cs
new file mode 100644
index 0000000..3113fd6
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/DeleteCommandFactory.cs
@@ -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 _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 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ExitCodes.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ExitCodes.cs
new file mode 100644
index 0000000..a064dbc
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ExitCodes.cs
@@ -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;
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/GetCommandFactory.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/GetCommandFactory.cs
new file mode 100644
index 0000000..6a62cd8
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/GetCommandFactory.cs
@@ -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 _keyArg = new("key")
+ {
+ Description = "The key whose value you want to retrieve."
+ };
+
+ private readonly Option _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 HandleAsync(ParseResult result, CancellationToken ct)
+ {
+ var key = result.GetRequiredValue(_keyArg);
+ var shouldPrintPretty = result.GetValue(_prettyOption);
+ var value = await _secureConfig.GetAsync(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;
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ICommandFactory.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ICommandFactory.cs
new file mode 100644
index 0000000..90437ca
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ICommandFactory.cs
@@ -0,0 +1,8 @@
+using System.CommandLine;
+
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
+
+internal interface ICommandFactory
+{
+ Command Create();
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ITerminal.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ITerminal.cs
new file mode 100644
index 0000000..206350d
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/ITerminal.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
+
+internal interface ITerminal
+{
+ public void WriteLine(string value);
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/SetCommandFactory.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/SetCommandFactory.cs
new file mode 100644
index 0000000..3c7e753
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/SetCommandFactory.cs
@@ -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 _keyArg = new("key")
+ {
+ Description = "The key whose value you want to set."
+ };
+
+ private readonly Argument _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 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;
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/Terminal.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/Terminal.cs
new file mode 100644
index 0000000..a585421
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Commands/Terminal.cs
@@ -0,0 +1,9 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Commands;
+
+internal sealed class Terminal : ITerminal
+{
+ public void WriteLine(string value)
+ {
+ Console.WriteLine(value);
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/Base64KeyOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/Base64KeyOptions.cs
new file mode 100644
index 0000000..80b79de
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/Base64KeyOptions.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
+
+internal sealed class Base64KeyOptions
+{
+ public string Key { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderOptions.cs
new file mode 100644
index 0000000..4ffa137
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderOptions.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
+
+internal sealed class CryptoProviderOptions
+{
+ public string Type { get; set; } = CryptoProviderTypes.Aes;
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderTypes.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderTypes.cs
new file mode 100644
index 0000000..d0d0769
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/CryptoProviderTypes.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
+
+internal static class CryptoProviderTypes
+{
+ public const string Aes = "aes";
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/JsonStorageOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/JsonStorageOptions.cs
new file mode 100644
index 0000000..2542323
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/JsonStorageOptions.cs
@@ -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";
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderOptions.cs
new file mode 100644
index 0000000..e8b518c
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderOptions.cs
@@ -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();
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderTypes.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderTypes.cs
new file mode 100644
index 0000000..b938c2d
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/KeyProviderTypes.cs
@@ -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";
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigBuilderExtensions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigBuilderExtensions.cs
new file mode 100644
index 0000000..bcaaa9b
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigBuilderExtensions.cs
@@ -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.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigCliOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigCliOptions.cs
new file mode 100644
index 0000000..fe18e6d
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigCliOptions.cs
@@ -0,0 +1,7 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
+
+internal sealed class SecureConfigCliOptions
+{
+ public string DefaultProfile { get; set; } = "Default";
+ public Dictionary Profiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigProfile.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigProfile.cs
new file mode 100644
index 0000000..b504f38
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/SecureConfigProfile.cs
@@ -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();
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderOptions.cs
new file mode 100644
index 0000000..5cf236f
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderOptions.cs
@@ -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();
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderTypes.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderTypes.cs
new file mode 100644
index 0000000..f70ff14
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Configuration/StorageProviderTypes.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Cli.Configuration;
+
+internal static class StorageProviderTypes
+{
+ public const string Json = "json";
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Json/CliJsonContext.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Json/CliJsonContext.cs
new file mode 100644
index 0000000..4980035
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Json/CliJsonContext.cs
@@ -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
+{
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Program.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Program.cs
new file mode 100644
index 0000000..062be99
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/Program.cs
@@ -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("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();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddHostedService();
+ })
+ .RunConsoleAsync();
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/SecureConfigCli.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/SecureConfigCli.cs
new file mode 100644
index 0000000..0e0ce71
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/SecureConfigCli.cs
@@ -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("--profile")
+ {
+ Description = "Allows specifying which profile to use from the appsettings.json file.",
+ },
+ };
+
+ public SecureConfigCli(
+ string[] args,
+ IHostApplicationLifetime lifetime,
+ IEnumerable 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;
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/StevanFreeborn.Extensions.Configuration.Secure.Cli.csproj b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/StevanFreeborn.Extensions.Configuration.Secure.Cli.csproj
new file mode 100644
index 0000000..b7dbef1
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Cli/StevanFreeborn.Extensions.Configuration.Secure.Cli.csproj
@@ -0,0 +1,38 @@
+
+
+
+ secure-config
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ true
+
+ latest
+ All
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
index 43d966e..2b7fd41 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
@@ -3,4 +3,4 @@ namespace StevanFreeborn.Extensions.Configuration.Secure.Sample;
public sealed record ApiOptions
{
public string ApiKey { get; init; } = string.Empty;
-}
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/AppJsonContext.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/AppJsonContext.cs
index 29eccc9..945bb72 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/AppJsonContext.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/AppJsonContext.cs
@@ -7,4 +7,4 @@ namespace StevanFreeborn.Extensions.Configuration.Secure.Sample;
[JsonSerializable(typeof(SmtpSettings))]
internal partial class AppJsonContext : JsonSerializerContext
{
-}
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/DatabaseSettings.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/DatabaseSettings.cs
index c627a77..196317d 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/DatabaseSettings.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/DatabaseSettings.cs
@@ -5,4 +5,4 @@ public sealed record DatabaseSettings
public string ConnectionString { get; init; } = string.Empty;
public int Timeout { get; init; }
public int RetryCount { get; init; }
-}
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
index cdfe47e..37f305f 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
@@ -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;
@@ -438,4 +436,4 @@ sealed class EnvironmentVariableKeyProvider : IEncryptionKeyProvider
return SHA256.HashData(Encoding.UTF8.GetBytes(value));
}
-}
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/SmtpSettings.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/SmtpSettings.cs
index 7d52e0d..e917d40 100644
--- a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/SmtpSettings.cs
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/SmtpSettings.cs
@@ -7,4 +7,4 @@ public sealed record SmtpSettings
public string Username { get; init; } = string.Empty;
public string Password { get; init; } = string.Empty;
public bool UseSsl { get; init; }
-}
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.csproj b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.csproj
new file mode 100644
index 0000000..20dbc32
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests.csproj
@@ -0,0 +1,52 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+ true
+ ./TestResults/Coverage/
+ cobertura
+ [*]StevanFreeborn.Extensions.Configuration.Secure.Cli*
+ **/Program.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandFactoryTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandFactoryTests.cs
new file mode 100644
index 0000000..44b12c4
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandFactoryTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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("key")
+ {
+ Description = "The key whose value you want to remove."
+ },
+ };
+
+ var result = _sut.Create();
+
+ result.Should().BeEquivalentTo(expectedCommand, static o => o.IgnoringCyclicReferences());
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandTests.cs
new file mode 100644
index 0000000..2a3f95c
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/DeleteCommandTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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())).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())).ReturnsAsync(true);
+
+ var result = await _sut.Parse([expectedKey]).InvokeAsync();
+
+ result.Should().Be(0);
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandFactoryTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandFactoryTests.cs
new file mode 100644
index 0000000..ce1cd64
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandFactoryTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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("key")
+ {
+ Description = "The key whose value you want to retrieve."
+ },
+ new Option("--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();
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandTests.cs
new file mode 100644
index 0000000..dedb3b8
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/GetCommandTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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(expectedKey, It.IsAny()))
+ .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(expectedKey, It.IsAny()))
+ .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(expectedKey, It.IsAny()))
+ .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(expectedKey, It.IsAny()))
+ .ReturnsAsync(expectedValue);
+
+ var result = await _sut.Parse([expectedKey, "-p"]).InvokeAsync();
+
+ result.Should().Be(0);
+
+ _mockTerminal.Verify(m => m.WriteLine(expectedPrintedValue), Times.Once());
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandFactoryTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandFactoryTests.cs
new file mode 100644
index 0000000..c0e2588
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandFactoryTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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("key")
+ {
+ Description = "The key whose value you want to set.",
+ },
+ new Argument("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();
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandTests.cs
new file mode 100644
index 0000000..42ec805
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/SetCommandTests.cs
@@ -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 _mockSecureConfig = new();
+ private readonly Mock _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()), 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()), Times.Once());
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/TerminalTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/TerminalTests.cs
new file mode 100644
index 0000000..3f5b389
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Commands/TerminalTests.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Configuration/SecureConfigBuilderExtensionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Configuration/SecureConfigBuilderExtensionsTests.cs
new file mode 100644
index 0000000..50f086d
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/Configuration/SecureConfigBuilderExtensionsTests.cs
@@ -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 _mockBuilder = new();
+
+ [Fact]
+ public void ApplyProfile_WhenCalledWithValidOptions_ItShouldConfigureBuilder()
+ {
+ _mockBuilder.Setup(static m => m.UseJsonFileStorage(It.IsAny>()))
+ .Callback>(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())).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>()), 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();
+
+ 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();
+ 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().WithMessage("Storage provider type 'unsupported' is not supported.");
+ }
+
+ [Fact]
+ public void ApplyProfile_WhenUnsupportedKey_ItShouldThrowNotSupportedException()
+ {
+ var builderMock = new Mock();
+ 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().WithMessage("Key provider type 'unsupported' is not supported.");
+ }
+
+ [Fact]
+ public void ApplyProfile_WhenUnsupportedCrypto_ItShouldThrowNotSupportedException()
+ {
+ var builderMock = new Mock();
+ 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().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();
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/SecureConfigCliTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/SecureConfigCliTests.cs
new file mode 100644
index 0000000..355b863
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Cli.Tests/Unit/SecureConfigCliTests.cs
@@ -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 _mockLifetime = new();
+ private readonly Mock _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();
+ }
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs
index 0ccb65e..36e0e55 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Common/TempDirectory.cs
@@ -17,4 +17,4 @@ internal sealed class TempDirectory : IDisposable
Directory.Delete(Path, true);
}
}
-}
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs
index b1e8952..ad82f88 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigExtensionsTests.cs
@@ -1,4 +1,3 @@
-using System.Security.Cryptography;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
@@ -249,4 +248,4 @@ internal sealed class IntegrationTestConfig
[JsonSerializable(typeof(IntegrationTestConfig))]
internal partial class IntegrationTestJsonContext : JsonSerializerContext
{
-}
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs
index a087e5c..1817068 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Integration/SecureConfigOptionsIntegrationTests.cs
@@ -1,4 +1,3 @@
-using System.Security.Cryptography;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
@@ -258,4 +257,4 @@ internal sealed class TestApiOptions
[JsonSerializable(typeof(TestApiOptions))]
internal partial class OptionsTestJsonContext : JsonSerializerContext
{
-}
+}
\ No newline at end of file
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
index a330012..e80ee77 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
@@ -13,8 +13,12 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
-
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
@@ -23,6 +27,18 @@
+
+ true
+ ./TestResults/Coverage/
+ cobertura
+ [StevanFreeborn.Extensions.Configuration.Secure]*
+ **/Program.cs
+
+
+
+
+
+
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs
index f2219df..43078af 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigBuilderTests.cs
@@ -84,8 +84,7 @@ public class SecureConfigBuilderTests
{
var act = () => _sut.UseJsonFileStorage((Action)null!);
- act.Should().Throw()
- .WithParameterName("configure");
+ act.Should().Throw().WithParameterName("configure");
}
[Fact]
@@ -104,8 +103,7 @@ public class SecureConfigBuilderTests
{
var act = () => _sut.AddJsonAotContext(null!);
- act.Should().Throw()
- .WithParameterName("context");
+ act.Should().Throw().WithParameterName("context");
}
[Fact]
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs
index 7a29345..7a6fb78 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigSourceTests.cs
@@ -24,8 +24,7 @@ public class SecureConfigSourceTests
loggerFactory: _mockLoggerFactory.Object
);
- act.Should().Throw()
- .WithParameterName("storageProvider");
+ act.Should().Throw().WithParameterName("storageProvider");
}
[Fact]
@@ -37,8 +36,7 @@ public class SecureConfigSourceTests
loggerFactory: _mockLoggerFactory.Object
);
- act.Should().Throw()
- .WithParameterName("cryptoProvider");
+ act.Should().Throw().WithParameterName("cryptoProvider");
}
[Fact]
@@ -50,8 +48,7 @@ public class SecureConfigSourceTests
loggerFactory: null!
);
- act.Should().Throw()
- .WithParameterName("loggerFactory");
+ act.Should().Throw().WithParameterName("loggerFactory");
}
[Fact]
@@ -87,6 +84,7 @@ public class SecureConfigSourceTests
public void Build_WhenCalled_ItShouldCreateLoggerFromFactory()
{
var mockLogger = new Mock>();
+
_mockLoggerFactory
.Setup(f => f.CreateLogger(typeof(SecureConfigProvider).FullName!))
.Returns(mockLogger.Object);
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
index b684c6a..0bea547 100644
--- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Configuration/SecureConfigTests.cs
@@ -1,4 +1,3 @@
-using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;