diff --git a/src/RobinTTY.NordigenApiClient.Tests/LiveApi/CredentialTests.cs b/src/RobinTTY.NordigenApiClient.Tests/LiveApi/CredentialTests.cs
index a5dd405..d8cbdd0 100644
--- a/src/RobinTTY.NordigenApiClient.Tests/LiveApi/CredentialTests.cs
+++ b/src/RobinTTY.NordigenApiClient.Tests/LiveApi/CredentialTests.cs
@@ -2,6 +2,7 @@
using RobinTTY.NordigenApiClient.Models;
using RobinTTY.NordigenApiClient.Models.Requests;
using RobinTTY.NordigenApiClient.Models.Responses;
+using RobinTTY.NordigenApiClient.Tests.Shared;
namespace RobinTTY.NordigenApiClient.Tests.LiveApi;
@@ -29,7 +30,7 @@ public async Task MakeRequestWithInvalidCredentials()
Assert.Multiple(() =>
{
Assert.That(agreementsResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
- Assert.That(ErrorMatchesExpectation(agreementsResponse.Error!), Is.True);
+ AssertErrorMatchesExpectation(agreementsResponse.Error!);
});
// Returns InstitutionsError
@@ -37,7 +38,7 @@ public async Task MakeRequestWithInvalidCredentials()
Assert.Multiple(() =>
{
Assert.That(institutionResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
- Assert.That(ErrorMatchesExpectation(institutionResponse.Error!), Is.True);
+ AssertErrorMatchesExpectation(institutionResponse.Error!);
});
// Returns AccountsError
@@ -45,7 +46,7 @@ public async Task MakeRequestWithInvalidCredentials()
Assert.Multiple(() =>
{
Assert.That(balancesResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
- Assert.That(ErrorMatchesExpectation(balancesResponse.Error!), Is.True);
+ AssertErrorMatchesExpectation(balancesResponse.Error!);
Assert.That(
new object?[]
{
@@ -61,7 +62,7 @@ public async Task MakeRequestWithInvalidCredentials()
Assert.Multiple(() =>
{
Assert.That(balancesResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
- Assert.That(ErrorMatchesExpectation(createAgreementResponse.Error!), Is.True);
+ AssertErrorMatchesExpectation(createAgreementResponse.Error!);
Assert.That(new object?[]
{
createAgreementResponse.Error!.AccessScopeError, createAgreementResponse.Error.AccessValidForDaysError,
@@ -78,7 +79,7 @@ public async Task MakeRequestWithInvalidCredentials()
Assert.Multiple(() =>
{
Assert.That(balancesResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
- Assert.That(ErrorMatchesExpectation(requisitionResponse.Error!), Is.True);
+ AssertErrorMatchesExpectation(requisitionResponse.Error!);
Assert.That(new object?[]
{
requisitionResponse.Error!.AgreementError, requisitionResponse.Error.InstitutionIdError,
@@ -89,8 +90,7 @@ public async Task MakeRequestWithInvalidCredentials()
});
}
- private static bool ErrorMatchesExpectation(BasicResponse error)
- {
- return error is {Detail: "Authentication credentials were not provided.", Summary: "Authentication failed"};
- }
+ private static void AssertErrorMatchesExpectation(BasicResponse error) =>
+ AssertionHelpers.AssertBasicResponseMatchesExpectations(error, "Authentication failed",
+ "No active account found with the given credentials");
}
diff --git a/src/RobinTTY.NordigenApiClient/Models/Errors/AccountsError.cs b/src/RobinTTY.NordigenApiClient/Models/Errors/AccountsError.cs
index e016350..71f5be1 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Errors/AccountsError.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Errors/AccountsError.cs
@@ -31,6 +31,7 @@ public class AccountsError : BasicResponse
#endif
[JsonPropertyName("date_from")]
public BasicResponse? StartDateError { get; }
+
#if NET6_0_OR_GREATER
///
/// An error that was returned related to the
@@ -48,6 +49,12 @@ public class AccountsError : BasicResponse
#endif
[JsonPropertyName("date_to")]
public BasicResponse? EndDateError { get; }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public AccountsError(){}
+
#if NET6_0_OR_GREATER
///
/// Creates a new instance of .
diff --git a/src/RobinTTY.NordigenApiClient/Models/Errors/CreateAgreementError.cs b/src/RobinTTY.NordigenApiClient/Models/Errors/CreateAgreementError.cs
index abbe5a7..4ae401b 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Errors/CreateAgreementError.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Errors/CreateAgreementError.cs
@@ -43,6 +43,11 @@ public class CreateAgreementError : BasicResponse
///
[JsonPropertyName("agreement")]
public BasicResponse? AgreementError { get; }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public CreateAgreementError(){}
///
/// Creates a new instance of .
diff --git a/src/RobinTTY.NordigenApiClient/Models/Errors/CreateRequisitionError.cs b/src/RobinTTY.NordigenApiClient/Models/Errors/CreateRequisitionError.cs
index 14c3ae2..07a8885 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Errors/CreateRequisitionError.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Errors/CreateRequisitionError.cs
@@ -57,6 +57,11 @@ public class CreateRequisitionError : BasicResponse
///
[JsonPropertyName("institution_id")]
public BasicResponse? InstitutionIdError { get; }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public CreateRequisitionError(){}
///
/// Creates a new instance of .
diff --git a/src/RobinTTY.NordigenApiClient/Models/Errors/InstitutionsError.cs b/src/RobinTTY.NordigenApiClient/Models/Errors/InstitutionsError.cs
index f9201b0..09684ac 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Errors/InstitutionsError.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Errors/InstitutionsError.cs
@@ -8,6 +8,11 @@ namespace RobinTTY.NordigenApiClient.Models.Errors;
///
public class InstitutionsError : BasicResponse
{
+ ///
+ /// Creates a new instance of .
+ ///
+ public InstitutionsError(){}
+
///
/// Creates a new instance of .
///
@@ -23,19 +28,12 @@ public InstitutionsError(BasicResponse country) : base(country.Summary, country.
/// Since this representation doesn't add any useful information (only extra encapsulation)
/// it is transformed to align this error with other errors returned by the API.
///
-internal class InstitutionsErrorInternal
+[method: JsonConstructor]
+internal class InstitutionsErrorInternal(BasicResponse? country, string? summary, string? detail)
{
- [JsonPropertyName("country")] public BasicResponse? Country { get; }
-
- [JsonPropertyName("summary")] public string? Summary { get; }
+ [JsonPropertyName("country")] public BasicResponse? Country { get; } = country;
- [JsonPropertyName("detail")] public string? Detail { get; }
+ [JsonPropertyName("summary")] public string? Summary { get; } = summary;
- [JsonConstructor]
- public InstitutionsErrorInternal(BasicResponse? country, string? summary, string? detail)
- {
- Country = country;
- Summary = summary;
- Detail = detail;
- }
+ [JsonPropertyName("detail")] public string? Detail { get; } = detail;
}
diff --git a/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs b/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs
index 314f86a..d192839 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Responses/BasicResponse.cs
@@ -11,14 +11,19 @@ public class BasicResponse
/// The summary text of the response/error.
///
[JsonPropertyName("summary")]
- public string? Summary { get; }
+ public string? Summary { get; init; }
///
/// The detailed description of the response/error.
///
[JsonPropertyName("detail")]
- public string? Detail { get; }
-
+ public string? Detail { get; init; }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public BasicResponse(){}
+
///
/// Creates a new instance of .
///
diff --git a/src/RobinTTY.NordigenApiClient/Models/Responses/ResponsePage.cs b/src/RobinTTY.NordigenApiClient/Models/Responses/ResponsePage.cs
index 6038a92..4b3c80c 100644
--- a/src/RobinTTY.NordigenApiClient/Models/Responses/ResponsePage.cs
+++ b/src/RobinTTY.NordigenApiClient/Models/Responses/ResponsePage.cs
@@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
-using RobinTTY.NordigenApiClient.Models.Errors;
namespace RobinTTY.NordigenApiClient.Models.Responses;
diff --git a/src/RobinTTY.NordigenApiClient/NordigenClient.cs b/src/RobinTTY.NordigenApiClient/NordigenClient.cs
index 54cd3aa..ec93b7c 100644
--- a/src/RobinTTY.NordigenApiClient/NordigenClient.cs
+++ b/src/RobinTTY.NordigenApiClient/NordigenClient.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System.Net;
+using System.Text.Json;
using RobinTTY.NordigenApiClient.Contracts;
using RobinTTY.NordigenApiClient.Endpoints;
using RobinTTY.NordigenApiClient.JsonConverters;
@@ -34,7 +35,7 @@ public class NordigenClient : INordigenClient
///
public IAccountsEndpoint AccountsEndpoint { get; }
-
+
///
public event EventHandler? TokenPairUpdated;
@@ -69,6 +70,18 @@ public NordigenClient(HttpClient httpClient, NordigenClientCredentials credentia
AccountsEndpoint = new AccountsEndpoint(this);
}
+ ///
+ /// Carries out the request to the GoCardless API, gathering a valid JWT if necessary.
+ ///
+ /// The URI of the API endpoint.
+ /// The to use for this request.
+ /// Token to signal cancellation of the operation.
+ /// Optional query parameters to add to the request.
+ /// Optional body to add to the request.
+ /// Whether to use authentication.
+ /// The type of the response.
+ /// The type of the error.
+ /// The response to the request.
internal async Task> MakeRequest(
string uri,
HttpMethod method,
@@ -76,16 +89,29 @@ internal async Task> MakeRequest>? query = null,
HttpContent? body = null,
bool useAuthentication = true
- ) where TResponse : class where TError : class
+ ) where TResponse : class where TError : BasicResponse, new()
{
var requestUri = query != null ? uri + UriQueryBuilder.GetQueryString(query) : uri;
HttpClient client;
+
+ // When an endpoint that requires authentication is called the client tries to update the JWT first
+ // - The updating is done using a semaphore to avoid multiple threads trying to update the token simultaneously
+ // - If the request to get the token succeeds, the subsequent request is executed
+ // - If the request to get the token fails, the error response from the token endpoint is returned instead
if (useAuthentication)
{
await TokenSemaphore.WaitAsync(cancellationToken);
+
try
{
- JsonWebTokenPair = await TryGetValidTokenPair(cancellationToken);
+ var tokenResponse = await TryGetValidTokenPair(cancellationToken);
+
+
+ if (tokenResponse.IsSuccess)
+ JsonWebTokenPair = tokenResponse.Result;
+ else
+ return new NordigenApiResponse(tokenResponse.StatusCode, tokenResponse.IsSuccess,
+ null, new TError {Summary = tokenResponse.Error.Summary, Detail = tokenResponse.Error.Detail});
}
finally
{
@@ -99,38 +125,47 @@ internal async Task> MakeRequest.FromHttpResponse(response, _serializerOptions,
cancellationToken);
}
+ private static async Task ExecuteRequest(HttpClient client, HttpMethod method,
+ string requestUri,
+ CancellationToken cancellationToken, HttpContent? body = null)
+ {
+ if (method == HttpMethod.Get)
+ return await client.GetAsync(requestUri, cancellationToken);
+ if (method == HttpMethod.Post)
+ return await client.PostAsync(requestUri, body, cancellationToken);
+ if (method == HttpMethod.Delete)
+ return await client.DeleteAsync(requestUri, cancellationToken);
+ if (method == HttpMethod.Put)
+ return await client.PutAsync(requestUri, body, cancellationToken);
+
+ throw new NotImplementedException();
+ }
+
///
/// Tries to retrieve a valid .
///
/// An optional token to signal cancellation of the operation.
///
- /// A valid if the operation was successful.
- /// Otherwise returns null.
+ /// A containing the or
+ /// the error of the operation.
///
- private async Task TryGetValidTokenPair(CancellationToken cancellationToken = default)
+ private async Task> TryGetValidTokenPair(
+ CancellationToken cancellationToken = default)
{
// Request a new token if it is null or if the refresh token has expired
if (JsonWebTokenPair == null || JsonWebTokenPair.RefreshToken.IsExpired(TimeSpan.FromMinutes(1)))
{
var response = await TokenEndpoint.GetTokenPair(cancellationToken);
- TokenPairUpdated?.Invoke(this, new TokenPairUpdatedEventArgs(response.Result));
- return response.Result;
+ if (response.IsSuccess)
+ TokenPairUpdated?.Invoke(this, new TokenPairUpdatedEventArgs(response.Result));
+
+ return response;
}
// Refresh the current access token if it's expired (or valid for less than a minute)
@@ -139,15 +174,21 @@ internal async Task> MakeRequest(response.StatusCode,
+ response.IsSuccess, token, response.Error);
+
+ if (token is not null)
+ TokenPairUpdated?.Invoke(this, new TokenPairUpdatedEventArgs(token));
+
+ return tokenPairResponse;
}
- // Token pair is still valid and can be returned
- return JsonWebTokenPair;
+ // Token pair is still valid and can be returned - wrap in NordigenApiResponse
+ return new NordigenApiResponse(HttpStatusCode.OK, true, JsonWebTokenPair,
+ null);
}
}
@@ -159,13 +200,13 @@ public class TokenPairUpdatedEventArgs : EventArgs
///
/// The updated .
///
- public JsonWebTokenPair? JsonWebTokenPair { get; set; }
+ public JsonWebTokenPair JsonWebTokenPair { get; set; }
///
/// Creates a new instance of .
///
/// The updated .
- public TokenPairUpdatedEventArgs(JsonWebTokenPair? jsonWebTokenPair)
+ public TokenPairUpdatedEventArgs(JsonWebTokenPair jsonWebTokenPair)
{
JsonWebTokenPair = jsonWebTokenPair;
}