diff --git a/src/SignhostAPIClient.Tests/IVerificationDeserializationTests.cs b/src/SignhostAPIClient.Tests/IVerificationDeserializationTests.cs new file mode 100644 index 00000000..7983a6d1 --- /dev/null +++ b/src/SignhostAPIClient.Tests/IVerificationDeserializationTests.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using FluentAssertions; +using Signhost.APIClient.Rest.DataObjects; +using Xunit; + +namespace Signhost.APIClient.Rest.Tests; + +public class IVerificationDeserializationTests +{ + [Fact] + public void Given_unknown_verification_type_When_deserializing_to_IVerification_Then_returns_unknown_verification() + { + // Arrange + const string json = "{\"Type\":\"SomethingUnknown\"}"; + + // Act + var verification = JsonSerializer.Deserialize(json, SignhostJsonSerializerOptions.Default); + + // Assert + var unknownVerification = verification.Should().BeOfType().Subject; + unknownVerification.Type.Should().Be("SomethingUnknown"); + } + + [Fact] + public void Given_unknown_verification_with_additional_fields_When_deserializing_to_IVerification_Then_additional_data_is_available() + { + // Arrange + const string json = "{\"Type\":\"SomethingUnknown\",\"DisplayName\":\"External\",\"Requires2FA\":true,\"Meta\":{\"Source\":\"partner\"}}"; + + // Act + var verification = JsonSerializer.Deserialize(json, SignhostJsonSerializerOptions.Default); + + // Assert + var unknownVerification = verification.Should().BeOfType().Subject; + unknownVerification.Type.Should().Be("SomethingUnknown"); + unknownVerification.AdditionalData.Should().NotBeNull(); + unknownVerification.AdditionalData!.Should().ContainKey("DisplayName"); + unknownVerification.AdditionalData["DisplayName"].GetString().Should().Be("External"); + unknownVerification.AdditionalData.Should().ContainKey("Requires2FA"); + unknownVerification.AdditionalData["Requires2FA"].GetBoolean().Should().BeTrue(); + unknownVerification.AdditionalData.Should().ContainKey("Meta"); + unknownVerification.AdditionalData["Meta"].GetProperty("Source").GetString().Should().Be("partner"); + } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs index 9c586474..918a4bed 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs @@ -1,22 +1,9 @@ using System.Text.Json.Serialization; +using Signhost.APIClient.Rest.JsonConverters; namespace Signhost.APIClient.Rest.DataObjects; -[JsonPolymorphic(TypeDiscriminatorPropertyName = "Type")] -[JsonDerivedType(typeof(ConsentVerification), "Consent")] -[JsonDerivedType(typeof(DigidVerification), "DigiD")] -[JsonDerivedType(typeof(EidasLoginVerification), "eIDAS Login")] -[JsonDerivedType(typeof(IdealVerification), "iDeal")] -[JsonDerivedType(typeof(IdinVerification), "iDIN")] -[JsonDerivedType(typeof(IPAddressVerification), "IPAddress")] -[JsonDerivedType(typeof(ItsmeIdentificationVerification), "itsme Identification")] -[JsonDerivedType(typeof(PhoneNumberVerification), "PhoneNumber")] -[JsonDerivedType(typeof(ScribbleVerification), "Scribble")] -[JsonDerivedType(typeof(SurfnetVerification), "SURFnet")] -[JsonDerivedType(typeof(CscVerification), "CSC Qualified")] -[JsonDerivedType(typeof(EherkenningVerification), "eHerkenning")] -[JsonDerivedType(typeof(OidcVerification), "OpenID Providers")] -[JsonDerivedType(typeof(OnfidoVerification), "Onfido")] +[JsonConverter(typeof(VerificationConverter))] public interface IVerification { } diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/UnknownVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/UnknownVerification.cs new file mode 100644 index 00000000..c02cb238 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/UnknownVerification.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest.DataObjects; + +public class UnknownVerification + : IVerification +{ + public string Type { get; set; } = default!; + + [JsonExtensionData] + public Dictionary? AdditionalData { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/JsonConverters/VerificationConverter.cs b/src/SignhostAPIClient/Rest/JsonConverters/VerificationConverter.cs new file mode 100644 index 00000000..477839d9 --- /dev/null +++ b/src/SignhostAPIClient/Rest/JsonConverters/VerificationConverter.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Signhost.APIClient.Rest.DataObjects; + +namespace Signhost.APIClient.Rest.JsonConverters; + +public class VerificationConverter + : JsonConverter +{ + public override IVerification Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var jsonElement = JsonSerializer.Deserialize(ref reader, options); + if (jsonElement.ValueKind is not JsonValueKind.Object) { + throw new JsonException("Verification must be a JSON object."); + } + + if ( + !jsonElement.TryGetProperty("Type", out var typeElement) || + typeElement.ValueKind is not JsonValueKind.String || + string.IsNullOrWhiteSpace(typeElement.GetString()) + ) { + throw new JsonException("Verification must contain a non-empty Type property."); + } + + string discriminator = typeElement.GetString()!; + string json = jsonElement.GetRawText(); + var verificationType = ResolveType(discriminator); + + if (verificationType is not null) { + return (IVerification)JsonSerializer.Deserialize(json, verificationType, options)!; + } + + return JsonSerializer.Deserialize(json, options)!; + } + + public override void Write(Utf8JsonWriter writer, IVerification value, JsonSerializerOptions options) + { + if (value is UnknownVerification unknownVerification) { + JsonSerializer.Serialize(writer, unknownVerification, options); + return; + } + + string discriminator = ResolveDiscriminator(value) + ?? throw new JsonException($"Verification type '{value.GetType().Name}' is not supported."); + + string json = JsonSerializer.Serialize(value, value.GetType(), options); + using var jsonDocument = JsonDocument.Parse(json); + + writer.WriteStartObject(); + writer.WriteString("Type", discriminator); + + foreach (var property in jsonDocument.RootElement.EnumerateObject()) { + writer.WritePropertyName(property.Name); + property.Value.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + private static Type? ResolveType(string discriminator) => discriminator switch { + "Consent" => typeof(ConsentVerification), + "DigiD" => typeof(DigidVerification), + "eIDAS Login" => typeof(EidasLoginVerification), + "iDeal" => typeof(IdealVerification), + "iDIN" => typeof(IdinVerification), + "IPAddress" => typeof(IPAddressVerification), + "itsme Identification" => typeof(ItsmeIdentificationVerification), + "PhoneNumber" => typeof(PhoneNumberVerification), + "Scribble" => typeof(ScribbleVerification), + "SURFnet" => typeof(SurfnetVerification), + "CSC Qualified" => typeof(CscVerification), + "eHerkenning" => typeof(EherkenningVerification), + "OpenID Providers" => typeof(OidcVerification), + "Onfido" => typeof(OnfidoVerification), + _ => null, + }; + + private static string? ResolveDiscriminator(IVerification value) => value switch { + ConsentVerification => "Consent", + DigidVerification => "DigiD", + EidasLoginVerification => "eIDAS Login", + IdealVerification => "iDeal", + IdinVerification => "iDIN", + IPAddressVerification => "IPAddress", + ItsmeIdentificationVerification => "itsme Identification", + PhoneNumberVerification => "PhoneNumber", + ScribbleVerification => "Scribble", + SurfnetVerification => "SURFnet", + CscVerification => "CSC Qualified", + EherkenningVerification => "eHerkenning", + OidcVerification => "OpenID Providers", + OnfidoVerification => "Onfido", + _ => null, + }; +}