Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Tools/dotnet-user-secrets/src/Internal/ListCommand.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Extensions.SecretManager.Tools.Internal;

Expand Down Expand Up @@ -53,14 +53,14 @@ public void Execute(CommandContext context)

private static void ReportJson(CommandContext context)
{
var jObject = new JObject();
var secrets = new Dictionary<string, string>();
foreach (var item in context.SecretStore.AsEnumerable())
{
jObject[item.Key] = item.Value;
secrets[item.Key] = item.Value;
}

context.Reporter.Output("//BEGIN");
context.Reporter.Output(jObject.ToString(Formatting.Indented));
context.Reporter.Output(JsonSerializer.Serialize(secrets, SecretsStore.SerializerOptions));
context.Reporter.Output("//END");
}
}
18 changes: 8 additions & 10 deletions src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Tools.Internal;
using Newtonsoft.Json.Linq;

namespace Microsoft.Extensions.SecretManager.Tools.Internal;

Expand All @@ -20,6 +20,11 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal;
/// </summary>
public class SecretsStore
{
internal static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
WriteIndented = true
};

private readonly string _secretsFilePath;
private readonly IDictionary<string, string> _secrets;

Expand Down Expand Up @@ -70,14 +75,7 @@ public virtual void Save()
{
Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));

var contents = new JObject();
if (_secrets != null)
{
foreach (var secret in _secrets.AsEnumerable())
{
contents[secret.Key] = secret.Value;
}
}
var contents = JsonSerializer.Serialize(_secrets, SerializerOptions);

// Create a temp file with the correct Unix file mode before moving it to the expected _filePath.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -86,7 +84,7 @@ public virtual void Save()
File.Move(tempFilename, _secretsFilePath, overwrite: true);
}
Comment thread
MichaelSimons marked this conversation as resolved.

File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
File.WriteAllText(_secretsFilePath, contents, Encoding.UTF8);
}

protected virtual IDictionary<string, string> Load(string userSecretsId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
</ItemGroup>

<ItemGroup>
<Reference Include="Newtonsoft.Json" />
<Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
</ItemGroup>

Expand Down
149 changes: 149 additions & 0 deletions src/Tools/dotnet-user-secrets/test/ListCommandTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Microsoft.Extensions.Tools.Internal;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.Extensions.SecretManager.Tools.Tests;

public class ListCommandTest
{
private readonly ITestOutputHelper _output;

public ListCommandTest(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public void List_Json_OutputIsProperlyFormatted()
{
var secretStore = new TestSecretsStore(_output);
secretStore.Set("key1", "value1");
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: true);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

Comment thread
MichaelSimons marked this conversation as resolved.
var output = testConsole.GetOutput();
var jsonContent = ExtractJsonContent(output);

Assert.Equal("{\n \"key1\": \"value1\"\n}", jsonContent, ignoreLineEndingDifferences: true);
}

[Fact]
public void List_Json_EscapesNonAsciiCharacters()
{
var secretStore = new TestSecretsStore(_output);
secretStore.Set("AzureAd:ClientSecret", "abcd郩˙î");
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: true);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

var output = testConsole.GetOutput();
var jsonContent = ExtractJsonContent(output);

// Non-ASCII characters are Unicode-escaped using the default System.Text.Json encoding
Assert.Equal("{\n \"AzureAd:ClientSecret\": \"abcd\\u00E9\\u0192\\u00A9\\u02D9\\u00EE\"\n}", jsonContent, ignoreLineEndingDifferences: true);
}

[Fact]
public void List_Json_HasBeginAndEndMarkers()
{
var secretStore = new TestSecretsStore(_output);
secretStore.Set("key1", "value1");
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: true);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

var output = testConsole.GetOutput();

Assert.Contains("//BEGIN", output);
Assert.Contains("//END", output);
var beginIndex = output.IndexOf("//BEGIN", StringComparison.Ordinal);
var endIndex = output.IndexOf("//END", StringComparison.Ordinal);
Assert.True(beginIndex < endIndex, "//BEGIN should appear before //END");
}

[Fact]
public void List_NonJson_OutputIsProperlyFormatted()
{
var secretStore = new TestSecretsStore(_output);
secretStore.Set("key1", "value1");
secretStore.Set("AzureAd:ClientSecret", "someSecret");
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: false);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

var output = testConsole.GetOutput();
Assert.Contains("key1 = value1", output);
Assert.Contains("AzureAd:ClientSecret = someSecret", output);
}
Comment thread
MichaelSimons marked this conversation as resolved.

[Fact]
public void List_NonJson_EmptyStore()
{
var secretStore = new TestSecretsStore(_output);
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: false);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

Assert.Contains(Resources.Error_No_Secrets_Found, testConsole.GetOutput());
}

[Fact]
public void List_Json_EmptyStore()
{
var secretStore = new TestSecretsStore(_output);
var testConsole = new TestConsole(_output);
var reporter = new ConsoleReporter(testConsole);
var command = new ListCommand(jsonOutput: true);

command.Execute(new CommandContext(secretStore, reporter, testConsole));

var output = testConsole.GetOutput();
var jsonContent = ExtractJsonContent(output);

Assert.Equal("{}", jsonContent, ignoreLineEndingDifferences: true);
}

private static string ExtractJsonContent(string output)
{
const string beginMarker = "//BEGIN";
const string endMarker = "//END";
var beginIndex = output.IndexOf(beginMarker, StringComparison.Ordinal) + beginMarker.Length;
var endIndex = output.IndexOf(endMarker, StringComparison.Ordinal);
return output[beginIndex..endIndex].Trim();
Comment thread
MichaelSimons marked this conversation as resolved.
Outdated
}

private sealed class TestSecretsStore : SecretsStore
{
public TestSecretsStore(ITestOutputHelper output)
: base("xyz", new TestReporter(output))
{
}

protected override IDictionary<string, string> Load(string userSecretsId)
{
return new Dictionary<string, string>();
}

public override void Save()
{
// noop
}
}
}
50 changes: 22 additions & 28 deletions src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,33 +235,36 @@ public void Remove_Is_Case_Insensitive()
Assert.Contains(Resources.Error_No_Secrets_Found, _console.GetOutput());
}

[Fact]
public void List_Json_OptionIsWired()
{
string id;
var projectPath = _fixture.GetTempSecretProject(out id);
var secretManager = CreateProgram();
secretManager.RunInternal("set", "key1", "value1", "-p", projectPath);

_console.ClearOutput();
secretManager.RunInternal("list", "--id", id, "--json");

var stdout = _console.GetOutput();
Assert.Contains("//BEGIN", stdout);
Assert.Contains("\"key1\": \"value1\"", stdout);
Assert.Contains("//END", stdout);
}

[Fact]
public void List_Flattens_Nested_Objects()
{
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);
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abc"" } }", Encoding.UTF8);
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath, "--verbose");
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _console.GetOutput());
}

[Fact]
public void List_Json()
{
string 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);
var secretManager = new Program(_console, Path.GetDirectoryName(projectPath));
secretManager.RunInternal("list", "--id", id, "--json");
var stdout = _console.GetOutput();
Assert.Contains("//BEGIN", stdout);
Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
Assert.Contains("//END", stdout);
secretManager.RunInternal("list", "-p", projectPath);

Assert.Contains("AzureAd:ClientSecret = abc", _console.GetOutput());
}

[Fact]
Expand All @@ -279,20 +282,11 @@ public void Set_Flattens_Nested_Objects()
Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _console.GetOutput());
var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
Assert.Equal(@"{
""AzureAd:ClientSecret"": ""¡™£¢∞""
""AzureAd:ClientSecret"": ""\u00A1\u2122\u00A3\u00A2\u221E""
}",
fileContents, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
}

[Fact]
public void List_Empty_Secrets_File()
{
var projectPath = _fixture.GetTempSecretProject();
var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath, "--verbose");
Assert.Contains(Resources.Error_No_Secrets_Found, _console.GetOutput());
}

[Theory]
[InlineData(false, true)]
[InlineData(false, false)]
Expand Down
Loading