Create initial prototype of dotnet-user-secrets with MSBuild support

This commit is contained in:
Nate McMaster 2016-10-07 09:56:08 -07:00 committed by Nate McMaster
parent 9744f94b39
commit 29df59b89c
20 changed files with 649 additions and 245 deletions

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ project.lock.json
.testPublish/
.build/
/.vs/
.vscode/
testWorkDir/
*.nuget.props
*.nuget.targets

View File

@ -0,0 +1,6 @@
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(Project)" />
<Target Name="_FindUserSecretsProperty">
<WriteLinesToFile File="$(OutputFile)" Lines="$(UserSecretsId)" />
</Target>
</Project>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -13,6 +14,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
public bool IsHelp { get; set; }
public string Project { get; set; }
public ICommand Command { get; set; }
public string Configuration { get; set; }
public static CommandLineOptions Parse(string[] args, IConsole console)
{
@ -34,6 +36,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true);
var optionConfig = app.Option("-c|--configuration <CONFIGURATION>", $"The project configuration to use. Defaults to {Constants.DefaultConfiguration}",
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.",
@ -55,6 +60,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
return null;
}
options.Configuration = optionConfig.Value();
options.Id = optionId.Value();
options.IsHelp = app.IsShowingInformation;
options.IsVerbose = optionVerbose.HasValue();

View File

@ -1,25 +0,0 @@
// 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;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
/// <summary>
/// An exception whose stack trace should be suppressed in console output
/// </summary>
public class GracefulException : Exception
{
public GracefulException()
{
}
public GracefulException(string message) : base(message)
{
}
public GracefulException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,61 @@
// 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 System.Linq;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
internal class MsBuildProjectFinder
{
private readonly string _directory;
public MsBuildProjectFinder(string directory)
{
if (string.IsNullOrEmpty(directory))
{
throw new ArgumentException(Resources.Common_StringNullOrEmpty, nameof(directory));
}
_directory = directory;
}
public string FindMsBuildProject(string project)
{
var projectPath = project ?? _directory;
if (!Path.IsPathRooted(projectPath))
{
projectPath = Path.Combine(_directory, projectPath);
}
if (Directory.Exists(projectPath))
{
var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly)
.Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
.ToList();
if (projects.Count > 1)
{
throw new GracefulException(Resources.FormatError_MultipleProjectsFound(projectPath));
}
if (projects.Count == 0)
{
throw new GracefulException(Resources.FormatError_NoProjectsFound(projectPath));
}
return projects[0];
}
if (!File.Exists(projectPath))
{
throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath));
}
return projectPath;
}
}
}

View File

@ -0,0 +1,130 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
public class ProjectIdResolver : IDisposable
{
private const string TargetsFileName = "FindUserSecretsProperty.targets";
private readonly ILogger _logger;
private readonly string _workingDirectory;
private readonly List<string> _tempFiles = new List<string>();
public ProjectIdResolver(ILogger logger, string workingDirectory)
{
_workingDirectory = workingDirectory;
_logger = logger;
}
public string Resolve(string project, string configuration = Constants.DefaultConfiguration)
{
var finder = new MsBuildProjectFinder(_workingDirectory);
var projectFile = finder.FindMsBuildProject(project);
_logger.LogDebug(Resources.Message_Project_File_Path, projectFile);
var targetFile = GetTargetFile();
var outputFile = Path.GetTempFileName();
_tempFiles.Add(outputFile);
var commandOutput = new List<string>();
var commandResult = Command.CreateDotNet("msbuild",
new[] {
targetFile,
"/nologo",
"/t:_FindUserSecretsProperty",
$"/p:Project={projectFile}",
$"/p:OutputFile={outputFile}",
$"/p:Configuration={configuration}"
})
.CaptureStdErr()
.CaptureStdOut()
.OnErrorLine(l => commandOutput.Add(l))
.OnOutputLine(l => commandOutput.Add(l))
.Execute();
if (commandResult.ExitCode != 0)
{
_logger.LogDebug(string.Join(Environment.NewLine, commandOutput));
throw new GracefulException(Resources.FormatError_ProjectFailedToLoad(projectFile));
}
var id = File.ReadAllText(outputFile)?.Trim();
if (string.IsNullOrEmpty(id))
{
throw new GracefulException(Resources.FormatError_ProjectMissingId(projectFile));
}
return id;
}
public void Dispose()
{
foreach (var file in _tempFiles)
{
TryDelete(file);
}
}
private string GetTargetFile()
{
var assemblyDir = Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location);
// targets should be in one of these locations, depending on test setup and tools installation
var searchPaths = new[]
{
AppContext.BaseDirectory,
assemblyDir, // next to assembly
Path.Combine(assemblyDir, "../../tools"), // inside the nupkg
};
var foundFile = searchPaths
.Select(dir => Path.Combine(dir, TargetsFileName))
.Where(File.Exists)
.FirstOrDefault();
if (foundFile != null)
{
return foundFile;
}
// This should only really happen during testing. Current build system doesn't give us a good way to ensure the
// test project has an always-up to date version of the targets file.
// TODO cleanup after we switch to an MSBuild system in which can specify "CopyToOutputDirectory: Always" to resolve this issue
var outputPath = Path.GetTempFileName();
using (var resource = GetType().GetTypeInfo().Assembly.GetManifestResourceStream(TargetsFileName))
using (var stream = new FileStream(outputPath, FileMode.Create))
{
resource.CopyTo(stream);
}
// cleanup
_tempFiles.Add(outputPath);
return outputPath;
}
private static void TryDelete(string file)
{
try
{
if (File.Exists(file))
{
File.Delete(file);
}
}
catch
{
// whatever
}
}
}
}

View File

@ -1,6 +1,7 @@
// 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.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;

View File

@ -26,6 +26,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
}
_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
_secrets = Load(userSecretsId);
}

View File

@ -4,6 +4,7 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;

View File

@ -16,16 +16,16 @@
<dependencies>
<group targetFramework=".NETCoreApp1.0">
<!-- MUST BE alphabetical -->
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" />
<dependency id="Microsoft.DotNet.Cli.Utils" version="$dep_1$" />
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_2$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_4$" />
<dependency id="Newtonsoft.Json" version="$dep_5$" />
<dependency id="System.Runtime.Serialization.Primitives" version="$dep_6$" />
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_3$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_4$" />
<dependency id="Microsoft.NETCore.App" version="$dep_5$" />
</group>
</dependencies>
</metadata>
<files>
<file src="FindUserSecretsProperty.targets" target="tools\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.dll" target="lib\netcoreapp1.0\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.runtimeconfig.json" target="lib/netcoreapp1.0\" />
</files>

View File

@ -2,15 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Extensions.SecretManager.Tools
{
@ -21,12 +16,16 @@ namespace Microsoft.Extensions.SecretManager.Tools
private readonly IConsole _console;
private readonly string _workingDirectory;
public Program()
: this(PhysicalConsole.Singleton, Directory.GetCurrentDirectory())
public static int Main(string[] args)
{
DebugHelper.HandleDebugSwitch(ref args);
int rc;
new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()).TryRun(args, out rc);
return rc;
}
internal Program(IConsole console, string workingDirectory)
public Program(IConsole console, string workingDirectory)
{
_console = console;
_workingDirectory = workingDirectory;
@ -65,33 +64,6 @@ namespace Microsoft.Extensions.SecretManager.Tools
}
}
public static int Main(string[] args)
{
HandleDebugFlag(ref args);
int rc;
new Program().TryRun(args, out rc);
return rc;
}
[Conditional("DEBUG")]
private static void HandleDebugFlag(ref string[] args)
{
for (var i = 0; i < args.Length; ++i)
{
if (args[i] == "--debug")
{
Console.WriteLine("Process ID " + Process.GetCurrentProcess().Id);
Console.WriteLine("Paused for debugger. Press ENTER to continue");
Console.ReadLine();
args = args.Take(i).Concat(args.Skip(i + 1)).ToArray();
return;
}
}
}
public bool TryRun(string[] args, out int returnCode)
{
try
@ -103,6 +75,11 @@ namespace Microsoft.Extensions.SecretManager.Tools
{
if (exception is GracefulException)
{
if (exception.InnerException != null)
{
Logger.LogInformation(exception.InnerException.Message);
}
Logger.LogError(exception.Message);
}
else
@ -134,65 +111,23 @@ namespace Microsoft.Extensions.SecretManager.Tools
CommandOutputProvider.LogLevel = LogLevel.Debug;
}
var userSecretsId = ResolveUserSecretsId(options);
var userSecretsId = ResolveId(options);
var store = new SecretsStore(userSecretsId, Logger);
var context = new CommandContext(store, Logger, _console);
var context = new Internal.CommandContext(store, Logger, _console);
options.Command.Execute(context);
return 0;
}
private string ResolveUserSecretsId(CommandLineOptions options)
internal string ResolveId(CommandLineOptions options)
{
if (!string.IsNullOrEmpty(options.Id))
{
return options.Id;
}
var projectPath = options.Project ?? _workingDirectory;
if (!Path.IsPathRooted(projectPath))
using (var resolver = new ProjectIdResolver(Logger, _workingDirectory))
{
projectPath = Path.Combine(_workingDirectory, projectPath);
}
if (!projectPath.EndsWith("project.json", StringComparison.OrdinalIgnoreCase))
{
projectPath = Path.Combine(projectPath, "project.json");
}
var fileInfo = new PhysicalFileInfo(new FileInfo(projectPath));
if (!fileInfo.Exists)
{
throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath));
}
Logger.LogDebug(Resources.Message_Project_File_Path, fileInfo.PhysicalPath);
return ReadUserSecretsId(fileInfo);
}
// TODO can use runtime API when upgrading to 1.1
private string ReadUserSecretsId(IFileInfo fileInfo)
{
if (fileInfo == null || !fileInfo.Exists)
{
throw new GracefulException($"Could not find file '{fileInfo.PhysicalPath}'");
}
using (var stream = fileInfo.CreateReadStream())
using (var streamReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(streamReader))
{
var obj = JObject.Load(jsonReader);
var userSecretsId = obj.Value<string>("userSecretsId");
if (string.IsNullOrEmpty(userSecretsId))
{
throw new GracefulException($"Could not find 'userSecretsId' in json file '{fileInfo.PhysicalPath}'");
}
return userSecretsId;
return resolver.Resolve(options.Project, options.Configuration);
}
}
}

View File

@ -10,6 +10,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
internal static string Common_StringNullOrEmpty
{
get { return GetString("Common_StringNullOrEmpty"); }
}
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
internal static string FormatCommon_StringNullOrEmpty()
{
return GetString("Common_StringNullOrEmpty");
}
/// <summary>
/// Command failed : {message}
/// </summary>
@ -60,6 +76,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
}
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string Error_MultipleProjectsFound
{
get { return GetString("Error_MultipleProjectsFound"); }
}
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string FormatError_MultipleProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
}
/// <summary>
/// No secrets configured for this application.
/// </summary>
@ -76,6 +108,38 @@ namespace Microsoft.Extensions.SecretManager.Tools
return GetString("Error_No_Secrets_Found");
}
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string Error_NoProjectsFound
{
get { return GetString("Error_NoProjectsFound"); }
}
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string FormatError_NoProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
}
/// <summary>
/// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
/// </summary>
internal static string Error_ProjectMissingId
{
get { return GetString("Error_ProjectMissingId"); }
}
/// <summary>
/// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.
/// </summary>
internal static string FormatError_ProjectMissingId(object project)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project);
}
/// <summary>
/// The project file '{path}' does not exist.
/// </summary>
@ -92,6 +156,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
}
/// <summary>
/// Could not load the MSBuild project '{project}'.
/// </summary>
internal static string Error_ProjectFailedToLoad
{
get { return GetString("Error_ProjectFailedToLoad"); }
}
/// <summary>
/// Could not load the MSBuild project '{project}'.
/// </summary>
internal static string FormatError_ProjectFailedToLoad(object project)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project);
}
/// <summary>
/// Project file path {project}.
/// </summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Common_StringNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or an empty string.</value>
</data>
<data name="Error_Command_Failed" xml:space="preserve">
<value>Command failed : {message}</value>
</data>
@ -127,12 +130,24 @@ Use the '--help' flag to see info.</value>
<data name="Error_Missing_Secret" xml:space="preserve">
<value>Cannot find '{key}' in the secret store.</value>
</data>
<data name="Error_MultipleProjectsFound" xml:space="preserve">
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
</data>
<data name="Error_No_Secrets_Found" xml:space="preserve">
<value>No secrets configured for this application.</value>
</data>
<data name="Error_NoProjectsFound" xml:space="preserve">
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
</data>
<data name="Error_ProjectMissingId" xml:space="preserve">
<value>Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.</value>
</data>
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
<value>The project file '{path}' does not exist.</value>
</data>
<data name="Error_ProjectFailedToLoad" xml:space="preserve">
<value>Could not load the MSBuild project '{project}'.</value>
</data>
<data name="Message_Project_File_Path" xml:space="preserve">
<value>Project file path {project}.</value>
</data>

View File

@ -4,7 +4,13 @@
"outputName": "dotnet-user-secrets",
"emitEntryPoint": true,
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
"keyFile": "../../tools/Key.snk",
"copyToOutput": "*.targets",
"embed": {
"mappings": {
"FindUserSecretsProperty.targets": "./FindUserSecretsProperty.targets"
}
}
},
"description": "Command line tool to manage user secrets for Microsoft.Extensions.Configuration.",
"packOptions": {
@ -16,18 +22,22 @@
"configuration",
"secrets",
"usersecrets"
]
],
"files": {
"mappings": {
"tools/FindUserSecretsProperty.targets": "FindUserSecretsProperty.targets"
}
}
},
"dependencies": {
"Microsoft.DotNet.Cli.Utils": "1.0.0-*",
"Microsoft.Extensions.CommandLineUtils": "1.1.0-*",
"Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-*",
"Microsoft.Extensions.Logging": "1.1.0-*",
"Microsoft.NETCore.App": {
"version": "1.1.0-*",
"type": "platform"
},
"Newtonsoft.Json": "9.0.1",
"System.Runtime.Serialization.Primitives": "4.3.0-*"
}
},
"frameworks": {
"netcoreapp1.0": {}

View File

@ -0,0 +1,86 @@
// 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 Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Xunit;
namespace Microsoft.Extensions.SecretsManager.Tools.Tests
{
public class MsBuildProjectFinderTest
{
[Theory]
[InlineData(".csproj")]
[InlineData(".vbproj")]
[InlineData(".fsproj")]
public void FindsSingleProject(string extension)
{
using (var files = new TemporaryFileProvider())
{
var filename = "TestProject" + extension;
files.Add(filename, "");
var finder = new MsBuildProjectFinder(files.Root);
Assert.Equal(Path.Combine(files.Root, filename), finder.FindMsBuildProject(null));
}
}
[Fact]
public void ThrowsWhenNoFile()
{
using (var files = new TemporaryFileProvider())
{
var finder = new MsBuildProjectFinder(files.Root);
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
[Fact]
public void DoesNotMatchXproj()
{
using (var files = new TemporaryFileProvider())
{
var finder = new MsBuildProjectFinder(files.Root);
files.Add("test.xproj", "");
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
[Fact]
public void ThrowsWhenMultipleFile()
{
using (var files = new TemporaryFileProvider())
{
files.Add("Test1.csproj", "");
files.Add("Test2.csproj", "");
var finder = new MsBuildProjectFinder(files.Root);
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
[Fact]
public void ThrowsWhenFileDoesNotExist()
{
using (var files = new TemporaryFileProvider())
{
var finder = new MsBuildProjectFinder(files.Root);
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject("test.csproj"));
}
}
[Fact]
public void ThrowsWhenRootDoesNotExist()
{
var files = new TemporaryFileProvider();
var finder = new MsBuildProjectFinder(files.Root);
files.Dispose();
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
}

View File

@ -5,68 +5,79 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.DotNet.Cli.Utils;
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;
namespace Microsoft.Extensions.SecretManager.Tools.Tests
{
public class SecretManagerTests : IDisposable
public class SecretManagerTests : IClassFixture<UserSecretsTestFixture>
{
private TestLogger _logger;
private Stack<Action> _disposables = new Stack<Action>();
private readonly UserSecretsTestFixture _fixture;
public SecretManagerTests(ITestOutputHelper output)
public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_logger = new TestLogger(output);
}
private string GetTempSecretProject()
private Program CreateProgram()
{
string id;
return GetTempSecretProject(out id);
}
private string GetTempSecretProject(out string userSecretsId)
{
var projectPath = UserSecretHelper.GetTempSecretProject(out userSecretsId);
_disposables.Push(() => UserSecretHelper.DeleteTempSecretProject(projectPath));
return projectPath;
}
public void Dispose()
{
while (_disposables.Count > 0)
return new Program(new TestConsole(), Directory.GetCurrentDirectory())
{
_disposables.Pop().Invoke();
}
Logger = _logger
};
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void Error_MissingId(string id)
{
var project = Path.Combine(_fixture.CreateProject(id), "TestProject.csproj");
var secretManager = CreateProgram();
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "-p", project));
Assert.Equal(Resources.FormatError_ProjectMissingId(project), ex.Message);
}
[Fact]
public void Error_InvalidProjectFormat()
{
var project = Path.Combine(_fixture.CreateProject("<"), "TestProject.csproj");
var secretManager = CreateProgram();
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "-p", project));
Assert.Equal(Resources.FormatError_ProjectFailedToLoad(project), ex.Message);
}
[Fact]
public void Error_Project_DoesNotExist()
{
var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json");
var secretManager = new Program(new TestConsole(), Directory.GetCurrentDirectory()) { Logger = _logger };
var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj");
var secretManager = CreateProgram();
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "--project", projectPath));
Assert.Equal(Resources.FormatError_ProjectPath_NotFound(projectPath), ex.Message);
}
[Fact]
public void SupportsRelativePaths()
{
var projectPath = GetTempSecretProject();
var projectPath = _fixture.GetTempSecretProject();
var cwd = Path.Combine(projectPath, "nested1");
Directory.CreateDirectory(cwd);
var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider };
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
secretManager.RunInternal("list", "-p", "../", "--verbose");
secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose");
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(projectPath, "project.json")), _logger.Messages);
Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _logger.Messages);
}
[Theory]
@ -82,7 +93,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
new KeyValuePair<string, string>("key2", string.Empty)
};
var projectPath = GetTempSecretProject();
var projectPath = _fixture.GetTempSecretProject();
var dir = fromCurrentDirectory
? projectPath
: Path.GetTempPath();
@ -141,8 +152,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void SetSecret_Update_Existing_Secret()
{
var projectPath = GetTempSecretProject();
var secretManager = new Program() { Logger = _logger };
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
@ -161,31 +172,31 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void SetSecret_With_Verbose_Flag()
{
string id;
var projectPath = GetTempSecretProject(out id);
string secretId;
var projectPath = _fixture.GetTempSecretProject(out secretId);
_logger.SetLevel(LogLevel.Debug);
var secretManager = new Program() { Logger = _logger };
var secretManager = CreateProgram();
secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "project.json")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(id)), _logger.Messages);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages);
Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
_logger.Messages.Clear();
secretManager.RunInternal("-v", "list", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "project.json")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(id)), _logger.Messages);
Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages);
Assert.Contains("secret1 = value1", _logger.Messages);
}
[Fact]
public void Remove_Non_Existing_Secret()
{
var projectPath = GetTempSecretProject();
var secretManager = new Program() { Logger = _logger };
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages);
@ -194,8 +205,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void Remove_Is_Case_Insensitive()
{
var projectPath = GetTempSecretProject();
var secretManager = new Program() { Logger = _logger };
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains("SeCreT1 = value", _logger.Messages);
@ -211,12 +222,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void List_Flattens_Nested_Objects()
{
string id;
var projectPath = GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
string secretId;
var projectPath = _fixture.GetTempSecretProject(out secretId);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program() { Logger = _logger };
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
@ -231,7 +242,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
Out = new StringWriter(output)
};
string id;
var projectPath = GetTempSecretProject(out id);
var projectPath = _fixture.GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
@ -246,12 +257,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void Set_Flattens_Nested_Objects()
{
string id;
var projectPath = GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
string secretId;
var projectPath = _fixture.GetTempSecretProject(out secretId);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program() { Logger = _logger };
var secretManager = CreateProgram();
secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
secretManager.RunInternal("list", "-p", projectPath);
@ -268,8 +279,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact]
public void List_Empty_Secrets_File()
{
var projectPath = GetTempSecretProject();
var secretManager = new Program() { Logger = _logger };
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
@ -280,7 +291,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[InlineData(false)]
public void Clear_Secrets(bool fromCurrentDirectory)
{
var projectPath = GetTempSecretProject();
var projectPath = _fixture.GetTempSecretProject();
var dir = fromCurrentDirectory
? projectPath

View File

@ -75,7 +75,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretStore = new TestSecretsStore();
var command = new SetCommand("key", null);
var ex = Assert.Throws<GracefulException>(
var ex = Assert.Throws< Microsoft.DotNet.Cli.Utils.GracefulException>(
() => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)));
Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message);
}

View File

@ -0,0 +1,29 @@
// 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 System.Text;
namespace Microsoft.Extensions.SecretsManager.Tools.Tests
{
internal class TemporaryFileProvider : IDisposable
{
public TemporaryFileProvider()
{
Root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "tmpfiles", Guid.NewGuid().ToString())).FullName;
}
public string Root { get; }
public void Add(string filename, string contents)
{
File.WriteAllText(Path.Combine(Root, filename), contents, Encoding.UTF8);
}
public void Dispose()
{
Directory.Delete(Root, recursive: true);
}
}
}

View File

@ -1,47 +0,0 @@
// 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 Newtonsoft.Json;
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
{
public class UserSecretHelper
{
internal static string GetTempSecretProject()
{
string userSecretsId;
return GetTempSecretProject(out userSecretsId);
}
internal static string GetTempSecretProject(out string userSecretsId)
{
var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString()));
userSecretsId = Guid.NewGuid().ToString();
File.WriteAllText(
Path.Combine(projectPath.FullName, "project.json"),
JsonConvert.SerializeObject(new { userSecretsId }));
return projectPath.FullName;
}
internal static void SetTempSecretInProject(string projectPath, string userSecretsId)
{
File.WriteAllText(
Path.Combine(projectPath, "project.json"),
JsonConvert.SerializeObject(new { userSecretsId }));
}
internal static void DeleteTempSecretProject(string projectPath)
{
try
{
Directory.Delete(projectPath, true);
}
catch (Exception)
{
// Ignore failures.
}
}
}
}

View File

@ -0,0 +1,103 @@
// 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.Collections.Generic;
using System.IO;
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
{
public class UserSecretsTestFixture : IDisposable
{
private Stack<Action> _disposables = new Stack<Action>();
public const string TestSecretsId = "b918174fa80346bbb7f4a386729c0eff";
public UserSecretsTestFixture()
{
_disposables.Push(() => TryDelete(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId))));
}
public void Dispose()
{
while (_disposables.Count > 0)
{
_disposables.Pop()?.Invoke();
}
}
public string GetTempSecretProject()
{
string userSecretsId;
return GetTempSecretProject(out userSecretsId);
}
private const string ProjectTemplate = @"<Project ToolsVersion=""14.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<Import Project=""$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp1.0</TargetFrameworks>
{0}
</PropertyGroup>
<ItemGroup>
<Compile Include=""**\*.cs"" Exclude=""Excluded.cs"" />
<PackageReference Include=""Microsoft.NET.Sdk"" Version=""1.0.0-*"" PrivateAssets=""All"" />
<PackageReference Include=""Microsoft.NETCore.App"" Version=""1.0.0"" />
</ItemGroup>
<Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />
</Project>";
public string GetTempSecretProject(out string userSecretsId)
{
userSecretsId = Guid.NewGuid().ToString();
return CreateProject(userSecretsId);
}
public string CreateProject(string userSecretsId)
{
var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString()));
var prop = string.IsNullOrEmpty(userSecretsId)
? string.Empty
: $"<UserSecretsId>{userSecretsId}</UserSecretsId>";
File.WriteAllText(
Path.Combine(projectPath.FullName, "TestProject.csproj"),
string.Format(ProjectTemplate, prop));
var id = userSecretsId;
_disposables.Push(() =>
{
try
{
// may throw if id is bad
var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(id));
TryDelete(secretsDir);
}
catch { }
});
_disposables.Push(() => TryDelete(projectPath.FullName));
return projectPath.FullName;
}
private static void TryDelete(string directory)
{
try
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
}
catch (Exception)
{
// Ignore failures.
Console.WriteLine("Failed to delete " + directory);
}
}
}
}