Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 9 additions & 1 deletion src/AppConfigCli/Editor/StructuredEditHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Encodings.Web;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using AppConfigCli.Core;
Expand All @@ -16,7 +17,14 @@ public static string BuildJsonContent(IEnumerable<Item> visibleItems, string sep
.Where(i => i.State != ItemState.Deleted)
.ToDictionary(i => i.ShortKey, i => i.Value ?? string.Empty, StringComparer.Ordinal);
var root = FlatKeyMapper.BuildTree(flats, separator);
return JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
// Use relaxed encoder so ASCII characters like '+' are not escaped (e.g., '\u002B').
// This produces more natural JSON for editing in plain-text editors like Notepad.
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
return JsonSerializer.Serialize(root, jsonOptions);
}

public static (bool Ok, string Error, int Created, int Updated, int Deleted) ApplyJsonEdits(string json, string separator, List<Item> allItems, IEnumerable<Item> visibleUnderLabel, string? prefix, string? activeLabel)
Expand Down
15 changes: 13 additions & 2 deletions src/AppConfigCli/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
{
"profiles": {
"AppConfigCli": {
"Production": {
"commandName": "Project",
"commandLineArgs": "--auth cli",
"environmentVariables": {
"APP_CONFIG_ENDPOINT": "https://sh-neu-app-config.azconfig.io"
}
},
"Slask": {
"commandName": "Project",
"commandLineArgs": "--auth cli",
"environmentVariables": {
"APP_CONFIG_ENDPOINT": "https://sh-app-config-slask.azconfig.io"
}
},
"Choose Enpoint": {
"commandName": "Project",
"commandLineArgs": "--auth cli"
}
}
}
}
91 changes: 91 additions & 0 deletions tests/AppConfigCli.Tests/_StructuredEditHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,51 @@ private static List<Item> SeedDev()
};
}

[Fact]
public void build_json_does_not_escape_ascii_in_values()
{
// Arrange: sample similar to the user-provided example
var items = new List<Item>
{
new Item { FullKey = "MyConfigSection:SampleConnectionString", ShortKey = "MyConfigSection:SampleConnectionString", Label = "prod", OriginalValue = null, Value = "Data Source=database.example.com;Port=12345;Uid=dbuser;Password=my-Super+Secret/pa$$_w0rd#=;charset=utf8", State = ItemState.Unchanged },
new Item { FullKey = "MyConfigSection:Test", ShortKey = "MyConfigSection:Test", Label = "prod", OriginalValue = null, Value = "ConnectionString in prod mode. ", State = ItemState.Unchanged },
new Item { FullKey = "Settings:BackgroundColor", ShortKey = "Settings:BackgroundColor", Label = "prod", OriginalValue = null, Value = "brown", State = ItemState.Unchanged },
new Item { FullKey = "Settings:Message", ShortKey = "Settings:Message", Label = "prod", OriginalValue = null, Value = "Hello in Production override mode !!!!", State = ItemState.Unchanged },
};

// Visible under a single label as required by json editor flow
var visible = items.Where(i => i.Label == "prod");

// Act
var json = StructuredEditHelper.BuildJsonContent(visible, ":");

// Assert: ensure '+' and other ASCII are not unicode-escaped
json.Should().Contain("my-Super+Secret/pa$$_w0rd#=");
json.Should().NotContain("\\u002B"); // '+'
json.Should().NotContain("\\u002F"); // '/'
json.Should().NotContain("\\u0023"); // '#'

// Roundtrip apply should keep the exact value
var (ok, err, created, updated, deleted) = StructuredEditHelper.ApplyJsonEdits(json, ":", items, visible, prefix: string.Empty, activeLabel: "prod");
ok.Should().BeTrue(err);
items.Should().Contain(i => i.FullKey == "MyConfigSection:SampleConnectionString" && i.Label == "prod" && i.Value!.Contains("+Secret/"));
}

[Fact]
public void build_json_does_not_escape_ascii_in_property_names()
{
var items = new List<Item>
{
new Item { FullKey = "Foo+Bar:Baz", ShortKey = "Foo+Bar:Baz", Label = "prod", OriginalValue = null, Value = "v", State = ItemState.Unchanged }
};
var visible = items.Where(i => i.Label == "prod");
var json = StructuredEditHelper.BuildJsonContent(visible, ":");

// Property name should appear literally with '+'
json.Should().Contain("\"Foo+Bar\"");
json.Should().NotContain("\\u002B");
}

[Fact]
public void apply_json_invalid_top_level_returns_error()
{
Expand Down Expand Up @@ -74,4 +119,50 @@ public void apply_yaml_creates_updates_and_deletes()
items.Single(i => i.FullKey == "p:Color" && i.Label == "dev").Value.Should().Be("blue");
items.Single(i => i.FullKey == "p:Title" && i.Label == "dev").State.Should().Be(ItemState.Deleted);
}

[Fact]
public void apply_yaml_preserves_ascii_plus_and_slash_in_values()
{
// Arrange: start with a mismatched value so ApplyYamlEdits updates it
var items = new List<Item>
{
new Item { FullKey = "MyConfigSection:SampleConnectionString", ShortKey = "MyConfigSection:SampleConnectionString", Label = "prod", OriginalValue = null, Value = "WRONG", State = ItemState.Unchanged },
new Item { FullKey = "Settings:BackgroundColor", ShortKey = "Settings:BackgroundColor", Label = "prod", OriginalValue = null, Value = "brown", State = ItemState.Unchanged },
new Item { FullKey = "Settings:Message", ShortKey = "Settings:Message", Label = "prod", OriginalValue = null, Value = "Hello", State = ItemState.Unchanged },
};
var visible = items.Where(i => i.Label == "prod");

var desired = "Data Source=database.example.com;Port=12345;Uid=dbuser;Password=my-Super+Secret/pa$$_w0rd#=;charset=utf8";
var yaml =
"MyConfigSection:\n" +
" SampleConnectionString: \"" + desired + "\"\n" +
"Settings:\n" +
" BackgroundColor: brown\n" +
" Message: Hello in Production override mode !!!!\n";

// Act
var (ok, err, c, u, d) = StructuredEditHelper.ApplyYamlEdits(yaml, ":", items, visible, prefix: string.Empty, activeLabel: "prod");

// Assert
ok.Should().BeTrue(err);
u.Should().BeGreaterThan(0);
items.Should().Contain(i => i.FullKey == "MyConfigSection:SampleConnectionString" && i.Label == "prod" && i.Value == desired);
// Ensure '+' and '/' are present literally in the resulting value
items.Single(i => i.FullKey == "MyConfigSection:SampleConnectionString" && i.Label == "prod").Value!.Should().Contain("+Secret/");
}

[Fact]
public void apply_yaml_supports_plus_in_property_names()
{
var items = new List<Item>();
var visible = items.Where(i => i.Label == "prod");
var yaml =
"Foo+Bar:\n" +
" Baz: v\n";

var (ok, err, c, u, d) = StructuredEditHelper.ApplyYamlEdits(yaml, ":", items, visible, prefix: string.Empty, activeLabel: "prod");
ok.Should().BeTrue(err);
c.Should().Be(1);
items.Should().Contain(i => i.ShortKey == "Foo+Bar:Baz" && i.Value == "v");
}
}
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.2",
"version": "1.0",
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/tags/v\\d+\\.\\d+(?:\\.\\d+)?$"
Expand Down