diff --git a/src/StevanFreeborn.Atproto.IdentityResolver/Json/StringToCollectionConverter.cs b/src/StevanFreeborn.Atproto.IdentityResolver/Json/StringToCollectionConverter.cs new file mode 100644 index 0000000..c2539e9 --- /dev/null +++ b/src/StevanFreeborn.Atproto.IdentityResolver/Json/StringToCollectionConverter.cs @@ -0,0 +1,32 @@ +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StevanFreeborn.Atproto.IdentityResolver.Json; + +internal sealed class StringToCollectionConverter : JsonConverter> +{ + public override Collection? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + return reader.TokenType switch + { + JsonTokenType.Null => [], + JsonTokenType.String => [reader.GetString()!], + JsonTokenType.StartArray => JsonSerializer.Deserialize>(ref reader, options), + _ => throw new JsonException($"Unexpected token type '{reader.TokenType}' when deserializing {nameof(Collection<>)}.") + }; + } + + public override void Write( + Utf8JsonWriter writer, + Collection value, + JsonSerializerOptions options + ) + { + JsonSerializer.Serialize(writer, value, options); + } +} \ No newline at end of file diff --git a/tests/StevanFreeborn.Atproto.IdentityResolver.Tests/Unit/StringToCollectionConverterTests.cs b/tests/StevanFreeborn.Atproto.IdentityResolver.Tests/Unit/StringToCollectionConverterTests.cs new file mode 100644 index 0000000..3af83b5 --- /dev/null +++ b/tests/StevanFreeborn.Atproto.IdentityResolver.Tests/Unit/StringToCollectionConverterTests.cs @@ -0,0 +1,127 @@ +using System.Collections.ObjectModel; +using System.Text; +using System.Text.Json; + +using StevanFreeborn.Atproto.IdentityResolver.Json; + +namespace StevanFreeborn.Atproto.IdentityResolver.Tests.Unit; + +public sealed class StringToCollectionConverterTests +{ + private readonly StringToCollectionConverter _converter = new(); + private readonly JsonSerializerOptions _options = new(); + + private Collection? ReadJson(string json) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + return _converter.Read(ref reader, typeof(Collection), _options); + } + + private string WriteJson(Collection value) + { + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + _converter.Write(writer, value, _options); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + + [Fact] + public void Read_WhenJsonTokenIsNull_ItShouldReturnEmptyCollection() + { + var result = ReadJson("null"); + + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [Fact] + public void Read_WhenJsonTokenIsString_ItShouldReturnCollectionWithSingleString() + { + var result = ReadJson("\"https://w3id.org/did/v1\""); + + result.Should().NotBeNull(); + result.Should().ContainSingle().Which.Should().Be("https://w3id.org/did/v1"); + } + + [Fact] + public void Read_WhenJsonTokenIsEmptyString_ItShouldReturnCollectionWithEmptyString() + { + var result = ReadJson("\"\""); + + result.Should().NotBeNull(); + result.Should().ContainSingle().Which.Should().Be(string.Empty); + } + + [Fact] + public void Read_WhenJsonTokenIsArrayOfStrings_ItShouldReturnCollectionWithAllStrings() + { + var result = ReadJson("[\"https://w3id.org/did/v1\",\"https://w3id.org/security/suites/ed25519-2020/v1\"]"); + + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.Should().ContainInOrder("https://w3id.org/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"); + } + + [Fact] + public void Read_WhenJsonTokenIsEmptyArray_ItShouldReturnEmptyCollection() + { + var result = ReadJson("[]"); + + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [Theory] + [InlineData("123", "Number")] + [InlineData("true", "True")] + [InlineData("false", "False")] + [InlineData("{\"key\":\"value\"}", "StartObject")] + public void Read_WhenJsonTokenIsUnexpectedType_ItShouldThrowJsonException(string json, string expectedToken) + { + var act = () => ReadJson(json); + + var e = act.Should().Throw(); + + e.WithMessage($"Unexpected token type '{expectedToken}' when deserializing Collection."); + } + + [Fact] + public void Write_WhenCollectionContainsMultipleStrings_ItShouldSerializeAsJsonArray() + { + var collection = new Collection + { + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + }; + + var json = WriteJson(collection); + + json.Should().Be("[\"https://w3id.org/did/v1\",\"https://w3id.org/security/suites/ed25519-2020/v1\"]"); + } + + [Fact] + public void Write_WhenCollectionContainsSingleString_ItShouldSerializeAsJsonArray() + { + var collection = new Collection + { + "https://w3id.org/did/v1" + }; + + var json = WriteJson(collection); + + json.Should().Be("[\"https://w3id.org/did/v1\"]"); + } + + [Fact] + public void Write_WhenCollectionIsEmpty_ItShouldSerializeAsEmptyJsonArray() + { + var collection = new Collection(); + + var json = WriteJson(collection); + + json.Should().Be("[]"); + } +} \ No newline at end of file