diff --git a/src/RobinTTY.NordigenApiClient.Tests/LiveApi/Endpoints/AgreementsEndpointTests.cs b/src/RobinTTY.NordigenApiClient.Tests/LiveApi/Endpoints/AgreementsEndpointTests.cs index e52d802..ec8f187 100644 --- a/src/RobinTTY.NordigenApiClient.Tests/LiveApi/Endpoints/AgreementsEndpointTests.cs +++ b/src/RobinTTY.NordigenApiClient.Tests/LiveApi/Endpoints/AgreementsEndpointTests.cs @@ -14,11 +14,12 @@ public void Setup() _apiClient = TestHelpers.GetConfiguredClient(); } + #region RequestsWithSuccessfulResponse + /// /// Tests the paging mechanism of retrieving end user agreements. /// Creates 3 agreements, retrieves them using 3 s and deletes the agreements after. /// - /// [Test] public async Task GetAgreementsPaged() { @@ -78,7 +79,6 @@ public async Task GetAgreementsPaged() /// /// Tests the retrieval of one agreement via and string id. /// - /// [Test] public async Task GetAgreement() { @@ -104,7 +104,6 @@ public async Task GetAgreement() /// /// Tests the retrieval of an agreement with an invalid guid. /// - /// [Test] public async Task GetAgreementWithInvalidGuid() { @@ -118,7 +117,6 @@ public async Task GetAgreementWithInvalidGuid() /// /// Tests the creation and deletion of an end user agreement. /// - /// [Test] public async Task CreateAcceptAndDeleteAgreement() { @@ -153,42 +151,70 @@ public async Task CreateAcceptAndDeleteAgreement() Assert.That(deletionResponse.Result!.Summary, Is.EqualTo("End User Agreement deleted")); } + #endregion + + #region RequestsWithErrors + + /// + /// Tests the retrieving of an end user agreement with an invalid institution id. + /// + [Test] + public async Task GetAgreementWithInvalidInstitutionId() + { + var agreement = new CreateAgreementRequest(90, 90, + ["balances", "details", "transactions"], "invalid_institution"); + + var response = await _apiClient.AgreementsEndpoint.CreateAgreement(agreement); + + AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); + Assert.Multiple(() => + { + Assert.That(response.Error!.InstitutionIdError, Is.Not.Null); + Assert.That(response.Error!.InstitutionIdError!.Summary, + Is.EqualTo("Unknown Institution ID invalid_institution")); + Assert.That(response.Error!.InstitutionIdError!.Detail, + Is.EqualTo("Get Institution IDs from /institutions/?country={$COUNTRY_CODE}")); + }); + } + /// /// Tests the creation of an end user agreement with an invalid institution id. /// - /// [Test] public async Task CreateAgreementWithInvalidInstitutionId() { - var agreement = new CreateAgreementRequest(90, 90, ["balances", "details", "transactions"], - "SANDBOXFINANCE_SFIN000"); + var agreement = new CreateAgreementRequest(90, 90, + ["balances", "details", "transactions"], "invalid_institution"); + var response = await _apiClient.AgreementsEndpoint.CreateAgreement(agreement); - AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); - var result = response.Error!; - Assert.That(result.InstitutionIdError, Is.Not.Null); - Assert.That(result.InstitutionIdError!.Detail, - Is.EqualTo("Get Institution IDs from /institutions/?country={$COUNTRY_CODE}")); + AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); + Assert.Multiple(() => + { + Assert.That(response.Error!.InstitutionIdError, Is.Not.Null); + Assert.That(response.Error!.InstitutionIdError!.Summary, + Is.EqualTo("Unknown Institution ID invalid_institution")); + Assert.That(response.Error!.InstitutionIdError!.Detail, + Is.EqualTo("Get Institution IDs from /institutions/?country={$COUNTRY_CODE}")); + }); } /// /// Tests the creation of an end user agreement with invalid parameters. /// - /// [Test] public async Task CreateAgreementWithInvalidParams() { var agreement = new CreateAgreementRequest(200, 200, ["balances", "details", "transactions", "invalid", "invalid2"], "SANDBOXFINANCE_SFIN0000"); - var response = await _apiClient.AgreementsEndpoint.CreateAgreement(agreement); - AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); + var response = await _apiClient.AgreementsEndpoint.CreateAgreement(agreement); var result = response.Error!; + Assert.Multiple(() => { - Assert.That( - new[] {result.InstitutionIdError, result.AgreementError}, - Has.All.Null); + AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); + Assert.That(new[] {result.InstitutionIdError, result.AgreementError}, Has.All.Null); Assert.That(result.AccessScopeError!.Detail, Is.EqualTo("Choose one or several from ['balances', 'details', 'transactions']")); Assert.That(result.AccessValidForDaysError!.Detail, @@ -198,4 +224,25 @@ public async Task CreateAgreementWithInvalidParams() "max_historical_days must be > 0 and <= SANDBOXFINANCE_SFIN0000 transaction_total_days (90)")); }); } + + [Test] + public async Task CreateAgreementWithInvalidParamsAtPolishInstitution() + { + var agreement = new CreateAgreementRequest(90, 90, + ["balances", "transactions"], "PKO_BPKOPLPW"); + + var response = await _apiClient.AgreementsEndpoint.CreateAgreement(agreement); + var result = response.Error!; + + Assert.Multiple(() => + { + AssertionHelpers.AssertNordigenApiResponseIsUnsuccessful(response, HttpStatusCode.BadRequest); + Assert.That(new[] {result.InstitutionIdError, result.AgreementError}, Has.All.Null); + Assert.That(result.Detail, + Is.EqualTo("For this institution the following scopes are required together: ['details', 'balances']")); + Assert.That(result.Summary, Is.EqualTo("Institution access scope dependencies error")); + }); + } + + #endregion } diff --git a/src/RobinTTY.NordigenApiClient/JsonConverters/SingleOrArrayConverter.cs b/src/RobinTTY.NordigenApiClient/JsonConverters/SingleOrArrayConverter.cs index 097c306..b514e84 100644 --- a/src/RobinTTY.NordigenApiClient/JsonConverters/SingleOrArrayConverter.cs +++ b/src/RobinTTY.NordigenApiClient/JsonConverters/SingleOrArrayConverter.cs @@ -6,7 +6,7 @@ namespace RobinTTY.NordigenApiClient.JsonConverters; internal class SingleOrArrayConverter : JsonConverter where TEnumerable : IEnumerable { - public override TEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { @@ -20,6 +20,7 @@ internal class SingleOrArrayConverter : JsonConverter(ref reader, options); if (listItem != null) list.Add(listItem); } + return (TEnumerable) (IEnumerable) list; default: var item = JsonSerializer.Deserialize(ref reader, options); @@ -43,4 +44,4 @@ public override void Write(Utf8JsonWriter writer, TEnumerable value, JsonSeriali writer.WriteEndArray(); } } -} \ No newline at end of file +} diff --git a/src/RobinTTY.NordigenApiClient/JsonConverters/StringArrayMergeConverter.cs b/src/RobinTTY.NordigenApiClient/JsonConverters/StringArrayMergeConverter.cs new file mode 100644 index 0000000..508c238 --- /dev/null +++ b/src/RobinTTY.NordigenApiClient/JsonConverters/StringArrayMergeConverter.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using RobinTTY.NordigenApiClient.Models.Responses; + +namespace RobinTTY.NordigenApiClient.JsonConverters; + +/// +/// For some errors the GoCardless API returns arrays for Summary/Detail properties inside the . +/// I've never actually seen them contain multiple values, but this converter merges them into one string so that the +/// can stay as simple as possible. +/// +internal class StringArrayMergeConverter : JsonConverter +{ + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) break; + var listItem = JsonSerializer.Deserialize(ref reader, options); + if (listItem != null) list.Add(listItem); + } + + return string.Join("; ", list); + default: + return JsonSerializer.Deserialize(ref reader, options); + } + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} diff --git a/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs b/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs index d192839..9e2f2d0 100644 --- a/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs +++ b/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using RobinTTY.NordigenApiClient.JsonConverters; namespace RobinTTY.NordigenApiClient.Models.Responses; @@ -11,12 +12,14 @@ public class BasicResponse /// The summary text of the response/error. /// [JsonPropertyName("summary")] + [JsonConverter(typeof(StringArrayMergeConverter))] public string? Summary { get; init; } /// /// The detailed description of the response/error. /// [JsonPropertyName("detail")] + [JsonConverter(typeof(StringArrayMergeConverter))] public string? Detail { get; init; } ///