From 3bb058c0fe15f694ede4a409ac5759a8234adc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20=C3=85slund?= Date: Sun, 5 Oct 2025 14:19:14 +0200 Subject: [PATCH 1/3] fixed issue with escaped values in json display --- src/AppConfigCli/Editor/StructuredEditHelper.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/AppConfigCli/Editor/StructuredEditHelper.cs b/src/AppConfigCli/Editor/StructuredEditHelper.cs index 6b7b8bb..130f64d 100644 --- a/src/AppConfigCli/Editor/StructuredEditHelper.cs +++ b/src/AppConfigCli/Editor/StructuredEditHelper.cs @@ -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; @@ -16,7 +17,14 @@ public static string BuildJsonContent(IEnumerable 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 allItems, IEnumerable visibleUnderLabel, string? prefix, string? activeLabel) From a7dd73bbb86ffd9356d827d064a2402b248ba342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20=C3=85slund?= Date: Sun, 5 Oct 2025 14:36:16 +0200 Subject: [PATCH 2/3] Added unit test to prevent regressions --- .../_StructuredEditHelper.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/AppConfigCli.Tests/_StructuredEditHelper.cs b/tests/AppConfigCli.Tests/_StructuredEditHelper.cs index 8081553..6c2928b 100644 --- a/tests/AppConfigCli.Tests/_StructuredEditHelper.cs +++ b/tests/AppConfigCli.Tests/_StructuredEditHelper.cs @@ -15,6 +15,51 @@ private static List SeedDev() }; } + [Fact] + public void build_json_does_not_escape_ascii_in_values() + { + // Arrange: sample similar to the user-provided example + var items = new List + { + 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 + { + 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() { @@ -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 + { + 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(); + 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"); + } } From f47e51ea14593e7b2a01b63cd9536fda54d5fb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20=C3=85slund?= Date: Sun, 5 Oct 2025 14:48:44 +0200 Subject: [PATCH 3/3] Upgraded version to 1.0, since we are now feature complete and seem stable for others to test --- src/AppConfigCli/Properties/launchSettings.json | 15 +++++++++++++-- version.json | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/AppConfigCli/Properties/launchSettings.json b/src/AppConfigCli/Properties/launchSettings.json index 67bda83..c45765e 100644 --- a/src/AppConfigCli/Properties/launchSettings.json +++ b/src/AppConfigCli/Properties/launchSettings.json @@ -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" } } -} \ No newline at end of file +} diff --git a/version.json b/version.json index b854b10..79eff7b 100644 --- a/version.json +++ b/version.json @@ -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+)?$"