-
Notifications
You must be signed in to change notification settings - Fork 122
[Patch/32.2.2] feat: handle unknown enum values gracefully #1325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0939b5f
ci: restore dotnet 6 test pipeline for legacy versions
galesky-a 75ce0e0
feat: handle unknown enum values gracefully
galesky-a 601e89c
fix: import polymorphic deps
galesky-a 6c7185c
change tests from xunit to mstest
galesky-a 024f93c
fix: enum constructor uses nullable objects
galesky-a 76569ec
fix: remove unusued dep
galesky-a daaa4aa
test: explicitly test Issue 1228
galesky-a 8ac38ab
test: rename basic deseerializer
galesky-a cfacb6e
Address PR review: refactor enum tests and SafeStringEnumConverter
galesky-a 8040f27
Merge branch 'patch/32.1.3' into patch/32.2.2
galesky-a File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Manual CI for .NET 6.0 legacy support. | ||
| # Not auto-triggered: We only maintain v6.0 support for older version compatibility. | ||
| name: "Legacy Support: .NET 6.0 Build & Test" | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| dotnet6-build-and-unit-test: | ||
| name: Build & Test (.NET 6.0) - ${{ matrix.os }} | ||
| runs-on: ${{ matrix.os }} | ||
|
|
||
| strategy: | ||
| matrix: | ||
| os: [ ubuntu-latest, windows-latest ] | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup .NET SDKs | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| dotnet-version: | | ||
| 6.0.x | ||
| 8.0.x | ||
|
|
||
| - name: Restore | ||
| run: dotnet restore | ||
|
|
||
| - name: Build (Debug) | ||
| run: dotnet build --configuration Debug --framework net6.0 --no-restore | ||
|
|
||
| - name: Test | ||
| run: dotnet test --no-build --configuration Debug --framework net6.0 --no-restore Adyen.Test/Adyen.Test.csproj |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| using System; | ||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Converters; | ||
| using System.Runtime.Serialization; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| namespace Adyen.Test | ||
| { | ||
| [TestClass] | ||
| public class StringEnumConverterTest | ||
| { | ||
| [JsonConverter(typeof(StringEnumConverter))] | ||
| private enum TestEnum | ||
| { | ||
| [EnumMember(Value = "Value1")] | ||
| Value1 = 1, | ||
|
|
||
| [EnumMember(Value = "Value2")] | ||
| Value2 = 2 | ||
| } | ||
|
|
||
| private class TestModel | ||
| { | ||
| [JsonProperty("enumField")] | ||
| public TestEnum? EnumField { get; set; } | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [ExpectedException(typeof(JsonSerializationException))] | ||
| public void Given_StringEnumConverter_When_UnknownValue_Throws_JsonSerializationException() | ||
| { | ||
| var json = @"{""enumField"": ""UnknownValue""}"; | ||
| JsonConvert.DeserializeObject<TestModel>(json); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Given_StringEnumConverter_When_Null_Result_TestModel_EnumField_Is_Null() | ||
| { | ||
| var json = @"{""enumField"": null}"; | ||
|
|
||
| var result = JsonConvert.DeserializeObject<TestModel>(json); | ||
| Assert.IsNull(result.EnumField); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Given_StringEnumConverter_When_Value1_Returns_Value1_As_Enum() | ||
| { | ||
| var json = @"{""enumField"": ""Value1""}"; | ||
| var result = JsonConvert.DeserializeObject<TestModel>(json); | ||
|
|
||
| Assert.IsNotNull(result); | ||
| Assert.AreEqual(TestEnum.Value1, result.EnumField); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| using Adyen.Model.Checkout; | ||
| using Adyen.Model.TransferWebhooks; | ||
| using Adyen.Util; | ||
| using Adyen.Webhooks; | ||
| using Newtonsoft.Json; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
|
||
| namespace Adyen.Test | ||
| { | ||
| [TestClass] | ||
| public class UnknownEnumDeserializationTest | ||
| { | ||
| [TestMethod] | ||
| public void TestUnknownEnumValueDeserializesToNull() | ||
| { | ||
| // Test JSON with an unknown enum value | ||
| var json = @"{ | ||
| ""channel"": ""UnknownChannelValue"", | ||
| ""amount"": { | ||
| ""value"": 1000, | ||
| ""currency"": ""EUR"" | ||
| }, | ||
| ""reference"": ""test-ref"", | ||
| ""merchantAccount"": ""test-merchant"" | ||
| }"; | ||
|
|
||
| // This should not throw an exception when using JsonOperation.Deserialize | ||
| var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json); | ||
|
|
||
| // The unknown enum value should deserialize to null | ||
| Assert.IsNotNull(paymentRequest); | ||
| Assert.IsNull(paymentRequest.Channel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestKnownEnumValueDeserializesCorrectly() | ||
| { | ||
| // Test JSON with a known enum value | ||
| var json = @"{ | ||
| ""channel"": ""Web"", | ||
| ""amount"": { | ||
| ""value"": 1000, | ||
| ""currency"": ""EUR"" | ||
| }, | ||
| ""reference"": ""test-ref"", | ||
| ""merchantAccount"": ""test-merchant"" | ||
| }"; | ||
|
|
||
| var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json); | ||
|
|
||
| Assert.IsNotNull(paymentRequest); | ||
| Assert.IsNotNull(paymentRequest.Channel); | ||
| Assert.AreEqual(PaymentRequest.ChannelEnum.Web, paymentRequest.Channel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestMissingEnumValueDeserializesToNull() | ||
| { | ||
| // Test JSON without the enum field | ||
| var json = @"{ | ||
| ""amount"": { | ||
| ""value"": 1000, | ||
| ""currency"": ""EUR"" | ||
| }, | ||
| ""reference"": ""test-ref"", | ||
| ""merchantAccount"": ""test-merchant"" | ||
| }"; | ||
|
|
||
| var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json); | ||
|
|
||
| Assert.IsNotNull(paymentRequest); | ||
| Assert.IsNull(paymentRequest.Channel); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestMultipleUnknownEnumValues() | ||
| { | ||
| // Test JSON with multiple unknown enum values | ||
| var json = @"{ | ||
| ""channel"": ""FutureChannel"", | ||
| ""entityType"": ""UnknownEntityType"", | ||
| ""industryUsage"": ""newUsageType"", | ||
| ""amount"": { | ||
| ""value"": 1000, | ||
| ""currency"": ""EUR"" | ||
| }, | ||
| ""reference"": ""test-ref"", | ||
| ""merchantAccount"": ""test-merchant"" | ||
| }"; | ||
|
|
||
| var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json); | ||
|
|
||
| Assert.IsNotNull(paymentRequest); | ||
| Assert.IsNull(paymentRequest.Channel); | ||
| Assert.IsNull(paymentRequest.EntityType); | ||
| Assert.IsNull(paymentRequest.IndustryUsage); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestSerializationOfNullEnumValue() | ||
| { | ||
| // Create a payment request with null enum values | ||
| var paymentRequest = new PaymentRequest | ||
| { | ||
| Channel = null, | ||
| EntityType = null | ||
| }; | ||
|
|
||
| var json = JsonConvert.SerializeObject(paymentRequest); | ||
|
|
||
| // Null values should not be serialized (based on EmitDefaultValue = false) | ||
| Assert.IsFalse(json.Contains("\"channel\"")); | ||
| Assert.IsFalse(json.Contains("\"entityType\"")); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestTransferWebhookWithUnknownReasonEnum() | ||
| { | ||
| var json = @"{ | ||
| ""data"": { | ||
| ""balancePlatform"": ""N2F_SANDBOX"", | ||
| ""creationDate"": ""2026-01-14T14:26:59+01:00"", | ||
| ""id"": ""38EBJS69NWKG7GSL"", | ||
| ""accountHolder"": { | ||
| ""description"": ""AccountId : 9819 - LegalEntityId : 58019 - LegalEntityName : Sandbox 01"", | ||
| ""id"": ""AH32CM322322995MS737SFD8T"", | ||
| ""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4"" | ||
| }, | ||
| ""amount"": { ""currency"": ""EUR"", ""value"": 100 }, | ||
| ""balanceAccount"": { | ||
| ""description"": ""AccountId : 9819 - LegalEntityId : 58019 - LegalEntityName : Sandbox 01"", | ||
| ""id"": ""BA32CM322322995MS737TFDPH"", | ||
| ""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4"" | ||
| }, | ||
| ""category"": ""issuedCard"", | ||
| ""categoryData"": { | ||
| ""authorisationType"": ""finalAuthorisation"", | ||
| ""panEntryMode"": ""manual"", | ||
| ""processingType"": ""ecommerce"", | ||
| ""relayedAuthorisationData"": { ""metadata"": { ""RefusedByN2F"": ""RefusedByRelayedRules"" }, ""reference"": """" }, | ||
| ""schemeUniqueTransactionId"": ""MCS9S6YGY0114"", | ||
| ""type"": ""issuedCard"", | ||
| ""validationFacts"": [ | ||
| { ""result"": ""valid"", ""type"": ""accountLookup"" }, | ||
| { ""result"": ""valid"", ""type"": ""cardAuthentication"" }, | ||
| { ""result"": ""notValidated"", ""type"": ""mitAllowedMerchant"" }, | ||
| { ""result"": ""valid"", ""type"": ""paymentInstrumentFound"" }, | ||
| { ""result"": ""valid"", ""type"": ""transactionValidation"" }, | ||
| { ""result"": ""notApplicable"", ""type"": ""authorisedPaymentInstrumentUser"" }, | ||
| { ""result"": ""valid"", ""type"": ""screening"" }, | ||
| { ""result"": ""valid"", ""type"": ""partyScreening"" }, | ||
| { ""result"": ""valid"", ""type"": ""transactionRules"" }, | ||
| { ""result"": ""invalid"", ""type"": ""relayedAuthorisation"" }, | ||
| { ""result"": ""valid"", ""type"": ""inputExpiryDateCheck"" }, | ||
| { ""result"": ""valid"", ""type"": ""paymentInstrument"" }, | ||
| { ""result"": ""valid"", ""type"": ""cardholderAuthentication"" }, | ||
| { ""result"": ""valid"", ""type"": ""paymentInstrumentExpirationCheck"" }, | ||
| { ""result"": ""valid"", ""type"": ""balanceCheck"" }, | ||
| { ""result"": ""valid"", ""type"": ""paymentInstrumentActive"" }, | ||
| { ""result"": ""valid"", ""type"": ""realBalanceAvailable"" } | ||
| ] | ||
| }, | ||
| ""counterparty"": { | ||
| ""merchant"": { | ||
| ""acquirerId"": ""013445"", | ||
| ""mcc"": ""7999"", | ||
| ""merchantId"": ""526567789012346"", | ||
| ""nameLocation"": { ""city"": ""Amsterdam"", ""country"": ""NL"", ""name"": ""N2F_FR_SANDBOX_TEST"" }, | ||
| ""postalCode"": ""1011 DJ"", | ||
| ""city"": ""Amsterdam"", | ||
| ""country"": ""NETHERLANDS"", | ||
| ""name"": ""N2F_FR_SANDBOX_TEST"" | ||
| } | ||
| }, | ||
| ""createdAt"": ""2026-01-14T14:26:58+01:00"", | ||
| ""direction"": ""outgoing"", | ||
| ""paymentInstrument"": { ""id"": ""PI32CM722322B35NR3F6RBNN8"", ""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4"" }, | ||
| ""reason"": ""UNKNWON_REASON_HERE_000"", | ||
| ""reference"": ""229BZZ4LB5LGGWS5-MCS9S6YGY"", | ||
| ""status"": ""refused"", | ||
| ""type"": ""payment"", | ||
| ""balances"": [{ ""currency"": ""EUR"", ""received"": -100 }], | ||
| ""eventId"": ""EVJN4295V223223W5NR3GPKDS32BBH"", | ||
| ""events"": [ | ||
| { | ||
| ""amount"": { ""currency"": ""EUR"", ""value"": -100 }, | ||
| ""bookingDate"": ""2026-01-14T14:26:59+01:00"", | ||
| ""id"": ""EVJN42CSX223223W5NR3GPKDNB4DBC"", | ||
| ""mutations"": [{ ""currency"": ""EUR"", ""received"": -100 }], | ||
| ""originalAmount"": { ""currency"": ""EUR"", ""value"": -100 }, | ||
| ""status"": ""received"", | ||
| ""type"": ""accounting"" | ||
| }, | ||
| { | ||
| ""amount"": { ""currency"": ""EUR"", ""value"": 100 }, | ||
| ""bookingDate"": ""2026-01-14T14:26:59+01:00"", | ||
| ""id"": ""EVJN4295V223223W5NR3GPKDS32BBH"", | ||
| ""mutations"": [{ ""currency"": ""EUR"", ""received"": 100 }], | ||
| ""originalAmount"": { ""currency"": ""EUR"", ""value"": 100 }, | ||
| ""status"": ""refused"", | ||
| ""type"": ""accounting"" | ||
| } | ||
| ], | ||
| ""sequenceNumber"": 2, | ||
| ""transactionRulesResult"": { ""allHardBlockRulesPassed"": true, ""score"": 0 }, | ||
| ""updatedAt"": ""2026-01-14T14:26:59+01:00"" | ||
| }, | ||
| ""environment"": ""test"", | ||
| ""timestamp"": ""2026-01-14T13:27:01.805Z"", | ||
| ""type"": ""balancePlatform.transfer.updated"" | ||
| }"; | ||
|
|
||
| var handler = new BalancePlatformWebhookHandler(); | ||
| var transferNotificationRequest = handler.GetGenericBalancePlatformWebhook(json) as TransferNotificationRequest; | ||
|
|
||
| Assert.IsNotNull(transferNotificationRequest); | ||
| Assert.IsNotNull(transferNotificationRequest.Data); | ||
| Assert.IsNull(transferNotificationRequest.Data.Reason); // Issue #1228 | ||
| Assert.AreEqual(TransferData.StatusEnum.Refused, transferNotificationRequest.Data.Status); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| using System; | ||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Converters; | ||
|
|
||
| namespace Adyen.Util | ||
| { | ||
| /// <summary> | ||
| /// A custom JSON converter for enums that returns null for unknown values instead of throwing an exception. | ||
| /// This ensures forward compatibility when new enum values are added to the API. | ||
| /// </summary> | ||
| public class SafeStringEnumConverter : StringEnumConverter | ||
| { | ||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||
| { | ||
| try | ||
| { | ||
| return base.ReadJson(reader, objectType, existingValue, serializer); | ||
| } | ||
| catch (JsonSerializationException) | ||
| { | ||
| // If the enum value is unknown, return null for nullable enums | ||
| // This allows the application to continue processing even when encountering unknown enum values | ||
| if (Nullable.GetUnderlyingType(objectType) != null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| // For non-nullable enums, we still throw to maintain backward compatibility | ||
| throw; | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using multi-line raw string literals for JSON can introduce unintended whitespace (like newlines and indentation) which might cause issues with some JSON parsers. For better readability and to ensure the JSON is correctly formatted, it's recommended to define test JSON on a single line. This makes the test less prone to formatting errors. This feedback applies to other small JSON string definitions in this test file. For larger JSON payloads like in
TestTransferWebhookWithUnknownReasonEnum, consider moving them to an embedded resource file to keep the test code clean.