using System.Text.Json;
using System.Text.Json.Nodes;
using XFEExtension.NetCore.XUnit.Assertions;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Core.Runtime;
using XFEWorkflow.Infrastructure.Storage;
namespace XFEWorkflow.Test;
[TestFixture]
public sealed class ReadableJsonTests
{
private const string Characters = "中文,。𠮷 🌚😀🐟 👨👩👧👦 👩🏽💻 👍🏽 🇨🇳 1️⃣ ❤️ é";
[TestCase(false)]
[TestCase(true)]
public void DisplayKeepsChineseSupplementaryCharactersAndWholeEmojiSequences(bool indented)
{
var original = new JsonObject { [Characters] = Characters, ["array"] = new JsonArray(Characters, null, true, 12) };
var text = original.Display(indented);
Assert.Contains(Characters, text); Assert.False(text.Contains("\\u"));
Assert.True(JsonNode.DeepEquals(original, JsonNode.Parse(text)));
var serialized = WorkflowJson.SerializeDisplay(new { DisplayName = Characters }, indented);
Assert.Contains(Characters, serialized); Assert.Equal(Characters, JsonNode.Parse(serialized)!["displayName"]!.GetValue<string>());
Assert.Contains("\\u", JsonSerializer.Serialize(original, WorkflowJson.Options));
}
[Test]
public void LiteralEscapeTextQuotesBackslashesAndControlsRoundTripWithoutUnescapingUserContent()
{
var values = new[] { @"\u4e2d", @"\uD83D\uDE00", @"\\u4e2d", @"\\\u4e2d", "\\🌚", "\\\\🌚", "\"引号😀\"\\路径", "\n\r\t\b\f\0\u001f", "\u007f\u0085", "<>&中文🌚" };
foreach (var value in values)
{
var original = new JsonObject { [value] = value };
var text = original.Display();
Assert.True(JsonNode.DeepEquals(original, JsonNode.Parse(text)), text);
Assert.Equal(value, JsonValue.Create(value).Text());
}
Assert.Equal("{\"literal\":\"\\\\u4e2d\"}", new JsonObject { ["literal"] = @"\u4e2d" }.Display());
Assert.Contains("\\n", new JsonObject { ["line"] = "\n" }.Display());
}
[Test]
public void TextAndConversionNodesUseReadableJsonWithoutChangingOriginalTextOrTypes()
{
var value = new JsonObject { ["name"] = Characters, ["literal"] = @"\u4e2d" };
Assert.Contains(Characters, value.Text());
foreach (var target in new[] { "string", "json" })
{
var converted = WorkflowConversions.Convert(value, target)!.GetValue<string>();
Assert.Contains(Characters, converted); Assert.True(JsonNode.DeepEquals(value, JsonNode.Parse(converted)));
}
var literal = " 原文\n" + Characters + @" \uD83D\uDE00 $var ¥var ";
Assert.Equal(literal, WorkflowConversions.Convert(JsonValue.Create(literal), "string")!.GetValue<string>());
Assert.Equal(literal, JsonNode.Parse(WorkflowConversions.Convert(JsonValue.Create(literal), "json")!.GetValue<string>())!.GetValue<string>());
Assert.Equal("null", ((JsonNode?)null).Display()); Assert.Equal("", ((JsonNode?)null).Text());
Assert.Equal("null", WorkflowConversions.Convert(null, "json")!.GetValue<string>());
}
[Test]
public void ManagedSecretsAreRedactedInBothEscapedAndReadableJsonIncludingEmojiAndControls()
{
const string secret = "密钥🌚👩🏽💻\"\\\nvalue";
var redactor = new SecretRedactor(); redactor.Add(secret);
var payload = new JsonObject { ["nested"] = new JsonObject { ["secret"] = secret }, ["safe"] = Characters };
foreach (var json in new[] { payload.ToJsonString(), payload.Display(), WorkflowJson.SerializeDisplay(payload) })
{
var redacted = JsonNode.Parse(redactor.Redact(json))!;
Assert.Equal("***", redacted["nested"]!["secret"]!.GetValue<string>());
Assert.Equal(Characters, redacted["safe"]!.GetValue<string>());
}
Assert.Equal("***", redactor.Redact(secret)); Assert.Equal(secret, payload["nested"]!["secret"]!.GetValue<string>());
}
[Test]
public async Task JsonLogExportsKeepChineseEmojiMetadataAndLiteralEscapesInUtf8()
{
await using var workspace = new TestWorkspace();
var entry = new WorkflowLogEntry { LogText = new JsonObject { ["name"] = Characters, ["literal"] = @"\u4e2d" }.Display(), WorkflowName = "工作流 " + Characters };
var path = Path.Combine(workspace.Paths.Temp, "日志.json");
await WorkflowLogExporter.ExportAsync(path, [entry], "json", false, new());
var text = await File.ReadAllTextAsync(path); Assert.Contains(Characters, text); Assert.False(text.Contains("\\uD83"));
var restored = JsonSerializer.Deserialize<List<WorkflowLogEntry>>(text, WorkflowJson.Options)!.Single();
Assert.Equal(entry.LogText, restored.LogText); Assert.Equal(entry.WorkflowName, restored.WorkflowName);
Assert.Equal(@"\u4e2d", JsonNode.Parse(restored.LogText)!["literal"]!.GetValue<string>());
}
}
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEExtension.NetCore.XUnit.Assertions;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Core.Runtime;
using XFEWorkflow.Infrastructure.Storage;
namespace XFEWorkflow.Test;
[TestFixture]
public sealed class ReadableJsonTests
{
private const string Characters = "中文,。𠮷 🌚😀🐟 👨👩👧👦 👩🏽💻 👍🏽 🇨🇳 1️⃣ ❤️ é";
[TestCase(false)]
[TestCase(true)]
public void DisplayKeepsChineseSupplementaryCharactersAndWholeEmojiSequences(bool indented)
{
var original = new JsonObject { [Characters] = Characters, ["array"] = new JsonArray(Characters, null, true, 12) };
var text = original.Display(indented);
Assert.Contains(Characters, text); Assert.False(text.Contains("\\u"));
Assert.True(JsonNode.DeepEquals(original, JsonNode.Parse(text)));
var serialized = WorkflowJson.SerializeDisplay(new { DisplayName = Characters }, indented);
Assert.Contains(Characters, serialized); Assert.Equal(Characters, JsonNode.Parse(serialized)!["displayName"]!.GetValue<string>());
Assert.Contains("\\u", JsonSerializer.Serialize(original, WorkflowJson.Options));
}
[Test]
public void LiteralEscapeTextQuotesBackslashesAndControlsRoundTripWithoutUnescapingUserContent()
{
var values = new[] { @"\u4e2d", @"\uD83D\uDE00", @"\\u4e2d", @"\\\u4e2d", "\\🌚", "\\\\🌚", "\"引号😀\"\\路径", "\n\r\t\b\f\0\u001f", "\u007f\u0085", "<>&中文🌚" };
foreach (var value in values)
{
var original = new JsonObject { [value] = value };
var text = original.Display();
Assert.True(JsonNode.DeepEquals(original, JsonNode.Parse(text)), text);
Assert.Equal(value, JsonValue.Create(value).Text());
}
Assert.Equal("{\"literal\":\"\\\\u4e2d\"}", new JsonObject { ["literal"] = @"\u4e2d" }.Display());
Assert.Contains("\\n", new JsonObject { ["line"] = "\n" }.Display());
}
[Test]
public void TextAndConversionNodesUseReadableJsonWithoutChangingOriginalTextOrTypes()
{
var value = new JsonObject { ["name"] = Characters, ["literal"] = @"\u4e2d" };
Assert.Contains(Characters, value.Text());
foreach (var target in new[] { "string", "json" })
{
var converted = WorkflowConversions.Convert(value, target)!.GetValue<string>();
Assert.Contains(Characters, converted); Assert.True(JsonNode.DeepEquals(value, JsonNode.Parse(converted)));
}
var literal = " 原文\n" + Characters + @" \uD83D\uDE00 $var ¥var ";
Assert.Equal(literal, WorkflowConversions.Convert(JsonValue.Create(literal), "string")!.GetValue<string>());
Assert.Equal(literal, JsonNode.Parse(WorkflowConversions.Convert(JsonValue.Create(literal), "json")!.GetValue<string>())!.GetValue<string>());
Assert.Equal("null", ((JsonNode?)null).Display()); Assert.Equal("", ((JsonNode?)null).Text());
Assert.Equal("null", WorkflowConversions.Convert(null, "json")!.GetValue<string>());
}
[Test]
public void ManagedSecretsAreRedactedInBothEscapedAndReadableJsonIncludingEmojiAndControls()
{
const string secret = "密钥🌚👩🏽💻\"\\\nvalue";
var redactor = new SecretRedactor(); redactor.Add(secret);
var payload = new JsonObject { ["nested"] = new JsonObject { ["secret"] = secret }, ["safe"] = Characters };
foreach (var json in new[] { payload.ToJsonString(), payload.Display(), WorkflowJson.SerializeDisplay(payload) })
{
var redacted = JsonNode.Parse(redactor.Redact(json))!;
Assert.Equal("***", redacted["nested"]!["secret"]!.GetValue<string>());
Assert.Equal(Characters, redacted["safe"]!.GetValue<string>());
}
Assert.Equal("***", redactor.Redact(secret)); Assert.Equal(secret, payload["nested"]!["secret"]!.GetValue<string>());
}
[Test]
public async Task JsonLogExportsKeepChineseEmojiMetadataAndLiteralEscapesInUtf8()
{
await using var workspace = new TestWorkspace();
var entry = new WorkflowLogEntry { LogText = new JsonObject { ["name"] = Characters, ["literal"] = @"\u4e2d" }.Display(), WorkflowName = "工作流 " + Characters };
var path = Path.Combine(workspace.Paths.Temp, "日志.json");
await WorkflowLogExporter.ExportAsync(path, [entry], "json", false, new());
var text = await File.ReadAllTextAsync(path); Assert.Contains(Characters, text); Assert.False(text.Contains("\\uD83"));
var restored = JsonSerializer.Deserialize<List<WorkflowLogEntry>>(text, WorkflowJson.Options)!.Single();
Assert.Equal(entry.LogText, restored.LogText); Assert.Equal(entry.WorkflowName, restored.WorkflowName);
Assert.Equal(@"\u4e2d", JsonNode.Parse(restored.LogText)!["literal"]!.GetValue<string>());
}
}