dotnet-user-secrets: add support for json output and piping json input

This commit is contained in:
Nate McMaster 2016-10-14 17:43:17 -07:00
Родитель 01d35b0624
Коммит a0e164f379
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: BD729980AA6A21BD
19 изменённых файлов: 450 добавлений и 64 удалений

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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 <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();

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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");
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<string, string> CurrentData => Data;
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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<string, string> _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<string, string> 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);
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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))

Просмотреть файл

@ -26,6 +26,24 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
}
/// <summary>
/// Missing parameter value for '{name}'.
/// Use the '--help' flag to see info.
/// </summary>
internal static string Error_MissingArgument
{
get { return GetString("Error_MissingArgument"); }
}
/// <summary>
/// Missing parameter value for '{name}'.
/// Use the '--help' flag to see info.
/// </summary>
internal static string FormatError_MissingArgument(object name)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
}
/// <summary>
/// Cannot find '{key}' in the secret store.
/// </summary>
@ -106,6 +124,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
}
/// <summary>
/// Successfully saved {number} secrets to the secret store.
/// </summary>
internal static string Message_Saved_Secrets
{
get { return GetString("Message_Saved_Secrets"); }
}
/// <summary>
/// Successfully saved {number} secrets to the secret store.
/// </summary>
internal static string FormatMessage_Saved_Secrets(object number)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
}
/// <summary>
/// Secrets file path {secretsFilePath}.
/// </summary>

Просмотреть файл

@ -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.

Просмотреть файл

@ -120,6 +120,10 @@
<data name="Error_Command_Failed" xml:space="preserve">
<value>Command failed : {message}</value>
</data>
<data name="Error_MissingArgument" xml:space="preserve">
<value>Missing parameter value for '{name}'.
Use the '--help' flag to see info.</value>
</data>
<data name="Error_Missing_Secret" xml:space="preserve">
<value>Cannot find '{key}' in the secret store.</value>
</data>
@ -135,6 +139,9 @@
<data name="Message_Saved_Secret" xml:space="preserve">
<value>Successfully saved {key} = {value} to the secret store.</value>
</data>
<data name="Message_Saved_Secrets" xml:space="preserve">
<value>Successfully saved {number} secrets to the secret store.</value>
</data>
<data name="Message_Secret_File_Path" xml:space="preserve">
<value>Secrets file path {secretsFilePath}.</value>
</data>

Просмотреть файл

@ -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<GracefulException>(() => 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<string, string>[]
{

Просмотреть файл

@ -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<GracefulException>(
() => 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<string, string> Load(string userSecretsId)
{
return new Dictionary<string, string>();
}
public override void Save()
{
// noop
}
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -7,7 +7,7 @@ using Newtonsoft.Json;
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
{
internal class UserSecretHelper
public class UserSecretHelper
{
internal static string GetTempSecretProject()
{