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
44 changes: 44 additions & 0 deletions src/SignhostAPIClient.Tests/IVerificationDeserializationTests.cs
Original file line number Diff line number Diff line change
@@ -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<IVerification>(json, SignhostJsonSerializerOptions.Default);

// Assert
var unknownVerification = verification.Should().BeOfType<UnknownVerification>().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<IVerification>(json, SignhostJsonSerializerOptions.Default);

// Assert
var unknownVerification = verification.Should().BeOfType<UnknownVerification>().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");
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
Original file line number Diff line number Diff line change
@@ -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<string, JsonElement>? AdditionalData { get; set; }
}
96 changes: 96 additions & 0 deletions src/SignhostAPIClient/Rest/JsonConverters/VerificationConverter.cs
Original file line number Diff line number Diff line change
@@ -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<IVerification>
{
public override IVerification Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonElement = JsonSerializer.Deserialize<JsonElement>(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<UnknownVerification>(json, options)!;
}

public override void Write(Utf8JsonWriter writer, IVerification value, JsonSerializerOptions options)
{
if (value is UnknownVerification unknownVerification) {
JsonSerializer.Serialize(writer, unknownVerification, options);

@adozhang-entrust adozhang-entrust Jul 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we return a specific warning for dev pick up what went wrong in splunk?
nvm will check unknownVerification in portal and log

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,
};
}