From a0e164f379c73010e41aea9a511ef66cc961be7e Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 14 Oct 2016 17:43:17 -0700 Subject: [PATCH] dotnet-user-secrets: add support for json output and piping json input --- .../Internal/ClearCommand.cs | 9 +- .../Internal/CommandContext.cs | 24 +++++ .../Internal/CommandLineOptions.cs | 16 ++- .../Internal/ICommand.cs | 6 +- .../Internal/IConsole.cs | 15 +++ .../Internal/ListCommand.cs | 46 ++++++-- .../Internal/PhysicalConsole.cs | 19 ++++ .../ReadableJsonConfigurationSource.cs | 18 ++++ .../Internal/RemoveCommand.cs | 12 +-- .../Internal/SecretsStore.cs | 28 +++-- .../Internal/SetCommand.cs | 89 +++++++++++---- .../Program.cs | 18 ++-- .../Properties/Resources.Designer.cs | 34 ++++++ .../README.md | 22 ++++ .../Resources.resx | 7 ++ .../SecretManagerTests.cs | 31 +++++- .../SetCommandTest.cs | 101 ++++++++++++++++++ .../TestConsole.cs | 17 +++ .../UserSecretHelper.cs | 2 +- 19 files changed, 450 insertions(+), 64 deletions(-) create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandContext.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/IConsole.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/PhysicalConsole.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/ReadableJsonConfigurationSource.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/README.md create mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs create mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/TestConsole.cs diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs index 41c6634..2569b6e 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs @@ -2,11 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.CommandLineUtils; -using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal class ClearCommand : ICommand + public class ClearCommand : ICommand { public static void Configure(CommandLineApplication command, CommandLineOptions options) { @@ -19,10 +18,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal }); } - public void Execute(SecretsStore store, ILogger logger) + public void Execute(CommandContext context) { - store.Clear(); - store.Save(); + context.SecretStore.Clear(); + context.SecretStore.Save(); } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandContext.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandContext.cs new file mode 100644 index 0000000..9503178 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + public class CommandContext + { + public CommandContext( + SecretsStore store, + ILogger logger, + IConsole console) + { + SecretStore = store; + Logger = logger; + Console = console; + } + + public IConsole Console { get; } + public ILogger Logger { get; } + public SecretsStore SecretStore { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs index 1b93392..3770542 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.IO; using System.Reflection; using Microsoft.Extensions.CommandLineUtils; @@ -9,16 +8,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal { public class CommandLineOptions { + public string Id { get; set; } public bool IsVerbose { get; set; } public bool IsHelp { get; set; } public string Project { get; set; } - internal ICommand Command { get; set; } + public ICommand Command { get; set; } - public static CommandLineOptions Parse(string[] args, TextWriter output) + public static CommandLineOptions Parse(string[] args, IConsole console) { var app = new CommandLineApplication() { - Out = output, + Out = console.Out, + Error = console.Error, Name = "dotnet user-secrets", FullName = "User Secrets Manager", Description = "Manages user secrets" @@ -33,7 +34,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal var optionProject = app.Option("-p|--project ", "Path to project, default is current directory", CommandOptionType.SingleValue, inherited: true); + // the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one + // in the current project + var optionId = app.Option("--id", "The user secret id to use.", + CommandOptionType.SingleValue, inherited: true); + var options = new CommandLineOptions(); + app.Command("set", c => SetCommand.Configure(c, options)); app.Command("remove", c => RemoveCommand.Configure(c, options)); app.Command("list", c => ListCommand.Configure(c, options)); @@ -48,6 +55,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal return null; } + options.Id = optionId.Value(); options.IsHelp = app.IsShowingInformation; options.IsVerbose = optionVerbose.HasValue(); options.Project = optionProject.Value(); diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs index 3d6035d..636c08a 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs @@ -1,12 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.Logging; - namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal interface ICommand + public interface ICommand { - void Execute(SecretsStore store, ILogger logger); + void Execute(CommandContext context); } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/IConsole.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/IConsole.cs new file mode 100644 index 0000000..819d477 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/IConsole.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + public interface IConsole + { + TextWriter Out { get; } + TextWriter Error { get; } + TextReader In { get; } + bool IsInputRedirected { get; } + } +} diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs index efc012c..16ee59a 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs @@ -3,35 +3,67 @@ using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal class ListCommand : ICommand + public class ListCommand : ICommand { + private readonly bool _jsonOutput; + public static void Configure(CommandLineApplication command, CommandLineOptions options) { command.Description = "Lists all the application secrets"; command.HelpOption(); + var optJson = command.Option("--json", "Use json output. JSON is wrapped by '//BEGIN' and '//END'", + CommandOptionType.NoValue); + command.OnExecute(() => { - options.Command = new ListCommand(); + options.Command = new ListCommand(optJson.HasValue()); }); } - public void Execute(SecretsStore store, ILogger logger) + public ListCommand(bool jsonOutput) { - if (store.Count == 0) + _jsonOutput = jsonOutput; + } + + public void Execute(CommandContext context) + { + if (_jsonOutput) { - logger.LogInformation(Resources.Error_No_Secrets_Found); + ReportJson(context); + return; + } + + if (context.SecretStore.Count == 0) + { + context.Logger.LogInformation(Resources.Error_No_Secrets_Found); } else { - foreach (var secret in store.AsEnumerable()) + foreach (var secret in context.SecretStore.AsEnumerable()) { - logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value)); + context.Logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value)); } } } + + private void ReportJson(CommandContext context) + { + var jObject = new JObject(); + foreach(var item in context.SecretStore.AsEnumerable()) + { + jObject[item.Key] = item.Value; + } + + // TODO logger would prefix each line. + context.Console.Out.WriteLine("//BEGIN"); + context.Console.Out.WriteLine(jObject.ToString(Formatting.Indented)); + context.Console.Out.WriteLine("//END"); + } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/PhysicalConsole.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/PhysicalConsole.cs new file mode 100644 index 0000000..6be8ff4 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/PhysicalConsole.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + public class PhysicalConsole : IConsole + { + private PhysicalConsole() { } + + public static IConsole Singleton { get; } = new PhysicalConsole(); + public TextWriter Error => Console.Error; + public TextReader In => Console.In; + public TextWriter Out => Console.Out; + public bool IsInputRedirected => Console.IsInputRedirected; + } +} diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ReadableJsonConfigurationSource.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ReadableJsonConfigurationSource.cs new file mode 100644 index 0000000..96d0554 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ReadableJsonConfigurationSource.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration.Json; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + public class ReadableJsonConfigurationProvider : JsonConfigurationProvider + { + public ReadableJsonConfigurationProvider() + : base(new JsonConfigurationSource()) + { + } + + public IDictionary CurrentData => Data; + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs index ff48c25..84910b9 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal class RemoveCommand : ICommand + public class RemoveCommand : ICommand { private readonly string _keyName; @@ -33,16 +33,16 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal _keyName = keyName; } - public void Execute(SecretsStore store, ILogger logger) + public void Execute(CommandContext context) { - if (!store.ContainsKey(_keyName)) + if (!context.SecretStore.ContainsKey(_keyName)) { - logger.LogWarning(Resources.Error_Missing_Secret, _keyName); + context.Logger.LogWarning(Resources.Error_Missing_Secret, _keyName); } else { - store.Remove(_keyName); - store.Save(); + context.SecretStore.Remove(_keyName); + context.SecretStore.Save(); } } } diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs index b25a757..67461d7 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs @@ -13,7 +13,7 @@ using Newtonsoft.Json.Linq; namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal class SecretsStore + public class SecretsStore { private readonly string _secretsFilePath; private IDictionary _secrets; @@ -27,13 +27,15 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath); + _secrets = Load(userSecretsId); + } - _secrets = new ConfigurationBuilder() - .AddJsonFile(_secretsFilePath, optional: true) - .Build() - .AsEnumerable() - .Where(i => i.Value != null) - .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); + public string this[string key] + { + get + { + return _secrets[key]; + } } public int Count => _secrets.Count; @@ -54,7 +56,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal } } - public void Save() + public virtual void Save() { Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath)); @@ -69,5 +71,15 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8); } + + protected virtual IDictionary Load(string userSecretsId) + { + return new ConfigurationBuilder() + .AddJsonFile(_secretsFilePath, optional: true) + .Build() + .AsEnumerable() + .Where(i => i.Value != null) + .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); + } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs index 15852ce..4799d9f 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs @@ -1,12 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Diagnostics; +using System.IO; +using System.Text; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; namespace Microsoft.Extensions.SecretManager.Tools.Internal { - internal class SetCommand : ICommand + public class SetCommand : ICommand { private readonly string _keyName; private readonly string _keyValue; @@ -14,38 +17,88 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal public static void Configure(CommandLineApplication command, CommandLineOptions options) { command.Description = "Sets the user secret to the specified value"; + command.ExtendedHelpText = @" +Additional Info: + This command will also handle piped input. Piped input is expected to be a valid JSON format. + +Examples: + dotnet user-secrets set ConnStr ""User ID=bob;Password=***"" + cat secrets.json | dotnet user-secrets set +"; + command.HelpOption(); - var keyArg = command.Argument("[name]", "Name of the secret"); + var nameArg = command.Argument("[name]", "Name of the secret"); var valueArg = command.Argument("[value]", "Value of the secret"); command.OnExecute(() => { - if (keyArg.Value == null) - { - throw new GracefulException("Missing parameter value for 'name'.\nUse the '--help' flag to see info."); - } - - if (valueArg.Value == null) - { - throw new GracefulException("Missing parameter value for 'value'.\nUse the '--help' flag to see info."); - } - - options.Command = new SetCommand(keyArg.Value, valueArg.Value); + options.Command = new SetCommand(nameArg.Value, valueArg.Value); }); } - public SetCommand(string keyName, string keyValue) + internal SetCommand(string keyName, string keyValue) { + Debug.Assert(keyName != null || keyValue == null, "Inconsistent state. keyValue must not be null if keyName is null."); _keyName = keyName; _keyValue = keyValue; } - public void Execute(SecretsStore store, ILogger logger) + internal SetCommand() + { } + + public void Execute(CommandContext context) { - store.Set(_keyName, _keyValue); - store.Save(); - logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue); + if (context.Console.IsInputRedirected && _keyName == null) + { + ReadFromInput(context); + } + else + { + SetFromArguments(context); + } + } + + private void ReadFromInput(CommandContext context) + { + // parses stdin with the same parser that Microsoft.Extensions.Configuration.Json would use + var provider = new ReadableJsonConfigurationProvider(); + using (var stream = new MemoryStream()) + { + using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true)) + { + writer.Write(context.Console.In.ReadToEnd()); // TODO buffer? + } + + stream.Seek(0, SeekOrigin.Begin); + provider.Load(stream); + } + + foreach (var k in provider.CurrentData) + { + context.SecretStore.Set(k.Key, k.Value); + } + + context.Logger.LogInformation(Resources.Message_Saved_Secrets, provider.CurrentData.Count); + + context.SecretStore.Save(); + } + + private void SetFromArguments(CommandContext context) + { + if (_keyName == null) + { + throw new GracefulException(Resources.FormatError_MissingArgument("name")); + } + + if (_keyValue == null) + { + throw new GracefulException((Resources.FormatError_MissingArgument("value"))); + } + + context.SecretStore.Set(_keyName, _keyValue); + context.SecretStore.Save(); + context.Logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue); } } } \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Program.cs b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs index 82422bd..cef3cad 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Program.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs @@ -18,17 +18,17 @@ namespace Microsoft.Extensions.SecretManager.Tools { private ILogger _logger; private CommandOutputProvider _loggerProvider; - private readonly TextWriter _consoleOutput; + private readonly IConsole _console; private readonly string _workingDirectory; public Program() - : this(Console.Out, Directory.GetCurrentDirectory()) + : this(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()) { } - internal Program(TextWriter consoleOutput, string workingDirectory) + internal Program(IConsole console, string workingDirectory) { - _consoleOutput = consoleOutput; + _console = console; _workingDirectory = workingDirectory; var loggerFactory = new LoggerFactory(); @@ -117,7 +117,7 @@ namespace Microsoft.Extensions.SecretManager.Tools internal int RunInternal(params string[] args) { - var options = CommandLineOptions.Parse(args, _consoleOutput); + var options = CommandLineOptions.Parse(args, _console); if (options == null) { @@ -136,12 +136,18 @@ namespace Microsoft.Extensions.SecretManager.Tools var userSecretsId = ResolveUserSecretsId(options); var store = new SecretsStore(userSecretsId, Logger); - options.Command.Execute(store, Logger); + var context = new CommandContext(store, Logger, _console); + options.Command.Execute(context); return 0; } private string ResolveUserSecretsId(CommandLineOptions options) { + if (!string.IsNullOrEmpty(options.Id)) + { + return options.Id; + } + var projectPath = options.Project ?? _workingDirectory; if (!Path.IsPathRooted(projectPath)) diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs index d9ae9fe..f8af024 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs @@ -26,6 +26,24 @@ namespace Microsoft.Extensions.SecretManager.Tools return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message); } + /// + /// Missing parameter value for '{name}'. + /// Use the '--help' flag to see info. + /// + internal static string Error_MissingArgument + { + get { return GetString("Error_MissingArgument"); } + } + + /// + /// Missing parameter value for '{name}'. + /// Use the '--help' flag to see info. + /// + internal static string FormatError_MissingArgument(object name) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name); + } + /// /// Cannot find '{key}' in the secret store. /// @@ -106,6 +124,22 @@ namespace Microsoft.Extensions.SecretManager.Tools return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value); } + /// + /// Successfully saved {number} secrets to the secret store. + /// + internal static string Message_Saved_Secrets + { + get { return GetString("Message_Saved_Secrets"); } + } + + /// + /// Successfully saved {number} secrets to the secret store. + /// + internal static string FormatMessage_Saved_Secrets(object number) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number); + } + /// /// Secrets file path {secretsFilePath}. /// diff --git a/src/Microsoft.Extensions.SecretManager.Tools/README.md b/src/Microsoft.Extensions.SecretManager.Tools/README.md new file mode 100644 index 0000000..088485f --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/README.md @@ -0,0 +1,22 @@ +dotnet-user-secrets +=================== + +`dotnet-user-secrets` is a command line tool for managing the secrets in a user secret store. + +### How To Install + +Add `Microsoft.Extensions.SecretManager.Tools` to the `tools` section of your `project.json` file: + +```js +{ + .. + "tools": { + "Microsoft.Extensions.SecretManager.Tools": "1.0.0-*" + } + ... +} +``` + +### How To Use + +Run `dotnet user-secrets --help` for more information about usage. \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx index 76631de..63d6550 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx +++ b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx @@ -120,6 +120,10 @@ Command failed : {message} + + Missing parameter value for '{name}'. +Use the '--help' flag to see info. + Cannot find '{key}' in the secret store. @@ -135,6 +139,9 @@ Successfully saved {key} = {value} to the secret store. + + Successfully saved {number} secrets to the secret store. + Secrets file path {secretsFilePath}. diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs index f9b8f62..082b53c 100644 --- a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs @@ -8,9 +8,9 @@ using System.Text; using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.UserSecrets.Tests; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.SecretManager.Tools.Internal; using Xunit; using Xunit.Abstractions; -using Microsoft.Extensions.SecretManager.Tools.Internal; namespace Microsoft.Extensions.SecretManager.Tools.Tests { @@ -48,7 +48,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests public void Error_Project_DoesNotExist() { var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json"); - var secretManager = new Program(Console.Out, Directory.GetCurrentDirectory()) { Logger = _logger }; + var secretManager = new Program(new TestConsole(), Directory.GetCurrentDirectory()) { Logger = _logger }; var ex = Assert.Throws(() => secretManager.RunInternal("list", "--project", projectPath)); @@ -61,7 +61,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests var projectPath = GetTempSecretProject(); var cwd = Path.Combine(projectPath, "nested1"); Directory.CreateDirectory(cwd); - var secretManager = new Program(Console.Out, cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider }; + var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider }; secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug; secretManager.RunInternal("list", "-p", "../", "--verbose"); @@ -86,7 +86,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests var dir = fromCurrentDirectory ? projectPath : Path.GetTempPath(); - var secretManager = new Program(Console.Out, dir) { Logger = _logger }; + var secretManager = new Program(new TestConsole(), dir) { Logger = _logger }; foreach (var secret in secrets) { @@ -222,6 +222,27 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages); } + [Fact] + public void List_Json() + { + var output = new StringBuilder(); + var testConsole = new TestConsole + { + Out = new StringWriter(output) + }; + string id; + var projectPath = GetTempSecretProject(out id); + var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); + Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); + File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); + var secretManager = new Program(testConsole, Path.GetDirectoryName(projectPath)) { Logger = _logger }; + secretManager.RunInternal("list", "--id", id, "--json"); + var stdout = output.ToString(); + Assert.Contains("//BEGIN", stdout); + Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout); + Assert.Contains("//END", stdout); + } + [Fact] public void Set_Flattens_Nested_Objects() { @@ -265,7 +286,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests ? projectPath : Path.GetTempPath(); - var secretManager = new Program(Console.Out, dir) { Logger = _logger }; + var secretManager = new Program(new TestConsole(), dir) { Logger = _logger }; var secrets = new KeyValuePair[] { diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs new file mode 100644 index 0000000..7fdd43b --- /dev/null +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Collections.Generic; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.SecretManager.Tools.Internal; +using Xunit; + +namespace Microsoft.Extensions.SecretManager.Tools.Tests +{ + public class SetCommandTest + { + [Fact] + public void SetsFromPipedInput() + { + var input = @" +{ + ""Key1"": ""str value"", +""Key2"": 1234, +""Key3"": false +}"; + var testConsole = new TestConsole + { + IsInputRedirected = true, + In = new StringReader(input) + }; + var secretStore = new TestSecretsStore(); + var command = new SetCommand(); + + command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)); + + Assert.Equal(3, secretStore.Count); + Assert.Equal("str value", secretStore["Key1"]); + Assert.Equal("1234", secretStore["Key2"]); + Assert.Equal("False", secretStore["Key3"]); + } + + [Fact] + public void ParsesNestedObjects() + { + var input = @" + { + ""Key1"": { + ""nested"" : ""value"" + }, + ""array"": [ 1, 2 ] + }"; + + var testConsole = new TestConsole + { + IsInputRedirected = true, + In = new StringReader(input) + }; + var secretStore = new TestSecretsStore(); + var command = new SetCommand(); + + command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)); + + Assert.Equal(3, secretStore.Count); + Assert.True(secretStore.ContainsKey("Key1:nested")); + Assert.Equal("value", secretStore["Key1:nested"]); + Assert.Equal("1", secretStore["array:0"]); + Assert.Equal("2", secretStore["array:1"]); + } + + [Fact] + public void OnlyPipesInIfNoArgs() + { + var testConsole = new TestConsole + { + IsInputRedirected = true, + In = new StringReader("") + }; + var secretStore = new TestSecretsStore(); + var command = new SetCommand("key", null); + + var ex = Assert.Throws( + () => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole))); + Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message); + } + + private class TestSecretsStore : SecretsStore + { + public TestSecretsStore() + : base("xyz", NullLogger.Instance) + { + } + + protected override IDictionary Load(string userSecretsId) + { + return new Dictionary(); + } + + public override void Save() + { + // noop + } + } + } +} diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestConsole.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestConsole.cs new file mode 100644 index 0000000..8370ecf --- /dev/null +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestConsole.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.Extensions.SecretManager.Tools.Internal; + +namespace Microsoft.Extensions.SecretManager.Tools.Tests +{ + public class TestConsole : IConsole + { + public TextWriter Error { get; set; } = Console.Error; + public TextReader In { get; set; } = Console.In; + public TextWriter Out { get; set; } = Console.Out; + public bool IsInputRedirected { get; set; } = false; + } +} diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs index 87dbddc..00932db 100644 --- a/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace Microsoft.Extensions.Configuration.UserSecrets.Tests { - internal class UserSecretHelper + public class UserSecretHelper { internal static string GetTempSecretProject() {