feat: Add IAgreementClient (#5)

This commit is contained in:
Valters Melnalksnis
2022-06-15 20:00:31 +03:00
parent cd0237c42c
commit b2b47e31f6
13 changed files with 359 additions and 34 deletions
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Options;
using NodaTime;
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Agreements;
using VMelnalksnis.NordigenDotNet.Institutions;
using VMelnalksnis.NordigenDotNet.Requisitions;
@@ -54,6 +55,7 @@ public static class ServiceCollectionExtensions
return serviceCollection
.AddTransient<INordigenClient, NordigenClient>()
.AddTransient<IAccountClient, AccountClient>()
.AddTransient<IAgreementClient, AgreementClient>()
.AddTransient<IInstitutionClient, InstitutionClient>()
.AddTransient<IRequisitionClient, RequisitionClient>()
.AddTransient(provider => provider.GetRequiredService<IOptionsSnapshot<NordigenOptions>>().Value)
@@ -0,0 +1,66 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <inheritdoc />
public sealed class AgreementClient : IAgreementClient
{
private readonly NordigenHttpClient _nordigenHttpClient;
/// <summary>Initializes a new instance of the <see cref="AgreementClient"/> class.</summary>
/// <param name="nordigenHttpClient">Http client configured for making requests to the Nordigen API.</param>
public AgreementClient(NordigenHttpClient nordigenHttpClient)
{
_nordigenHttpClient = nordigenHttpClient;
}
/// <inheritdoc />
public IAsyncEnumerable<EndUserAgreement> Get(int pageSize = 100, CancellationToken cancellationToken = default)
{
var requestUri = $"{Routes.Agreements.Uri}?limit={pageSize}&offset=0";
return _nordigenHttpClient.GetAsJsonPaginated<EndUserAgreement>(requestUri, cancellationToken);
}
/// <inheritdoc />
public async Task<EndUserAgreement> Get(Guid id, CancellationToken cancellationToken = default)
{
var agreement = await _nordigenHttpClient
.GetAsJson<EndUserAgreement>(Routes.Agreements.IdUri(id), cancellationToken)
.ConfigureAwait(false);
return agreement!;
}
/// <inheritdoc />
public async Task<EndUserAgreement> Post(EndUserAgreementCreation agreementCreation)
{
var agreement = await _nordigenHttpClient
.PostAsJson<EndUserAgreementCreation, EndUserAgreement>(Routes.Agreements.Uri, agreementCreation)
.ConfigureAwait(false);
return agreement!;
}
/// <inheritdoc />
public async Task Delete(Guid id)
{
await _nordigenHttpClient.Delete(Routes.Agreements.IdUri(id)).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<EndUserAgreement> Put(Guid id, EndUserAgreementAcceptance acceptance)
{
var agreement = await _nordigenHttpClient
.PutAsJson<EndUserAgreementAcceptance, EndUserAgreement>(Routes.Agreements.AcceptUri(id), acceptance)
.ConfigureAwait(false);
return agreement!;
}
}
@@ -0,0 +1,34 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using NodaTime;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details the end-user has agreement to share from a specific institution.</summary>
/// <param name="Id">The id of the end-user agreement.</param>
/// <param name="Created">The point in time when the agreement was created.</param>
/// <param name="MaxHistoricalDays">Maximum number of days of transaction data to retrieve.</param>
/// <param name="AccessValidForDays">Number of days from acceptance that the access can be used.</param>
/// <param name="AccessScope">Level of information to access.</param>
/// <param name="InstitutionId">An <see cref="Institution"/> ID for the agreement.</param>
public record EndUserAgreement(
Guid Id,
Instant Created,
[property: JsonPropertyName("max_historical_days")] int MaxHistoricalDays,
[property: JsonPropertyName("access_valid_for_days")] int AccessValidForDays,
[property: JsonPropertyName("access_scope")] List<string> AccessScope,
[property: JsonPropertyName("institution_id")] string InstitutionId)
{
/// <summary>The point int time when the end user accepted the agreement.</summary>
public Instant? Accepted { get; init; }
}
@@ -0,0 +1,14 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Text.Json.Serialization;
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details needed to accept an end-user agreement.</summary>
/// <param name="UserAgent">User agent string for the end user.</param>
/// <param name="IpAddress">End user IP address.</param>
public record EndUserAgreementAcceptance(
[property: JsonPropertyName("user_agent")] string UserAgent,
[property: JsonPropertyName("ip_address")] string IpAddress);
@@ -0,0 +1,56 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using VMelnalksnis.NordigenDotNet.Institutions;
#pragma warning disable SA1623
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Details needed to create a new end-user agreement.</summary>
public record EndUserAgreementCreation
{
/// <summary>Initializes a new instance of the <see cref="EndUserAgreementCreation"/> class.</summary>
/// <param name="institutionId">An <see cref="Institution"/> id for this agreement.</param>
/// <param name="maxHistoricalDays">Maximum number of days of transaction data to retrieve.</param>
/// <param name="accessValidForDays">Number of days from acceptance that the access can be used.</param>
/// <param name="accessScope">Level of information to access.</param>
public EndUserAgreementCreation(
string institutionId,
int maxHistoricalDays,
int accessValidForDays,
List<string> accessScope)
{
InstitutionId = institutionId;
MaxHistoricalDays = maxHistoricalDays;
AccessValidForDays = accessValidForDays;
AccessScope = accessScope;
}
/// <summary>Initializes a new instance of the <see cref="EndUserAgreementCreation"/> class with the default values.</summary>
/// <param name="institutionId">An <see cref="Institution"/> id for this agreement.</param>
public EndUserAgreementCreation(string institutionId)
: this(institutionId, 90, 90, new() { "balances", "details", "transactions" })
{
}
/// <summary>An <see cref="Institution"/> id for this agreement.</summary>
[JsonPropertyName("institution_id")]
public string InstitutionId { get; }
/// <summary>Maximum number of days of transaction data to retrieve.</summary>
[JsonPropertyName("max_historical_days")]
public int MaxHistoricalDays { get; }
/// <summary>Number of days from acceptance that the access can be used.</summary>
[JsonPropertyName("access_valid_for_days")]
public int AccessValidForDays { get; }
/// <summary>Level of information to access.</summary>
[JsonPropertyName("access_scope")]
public List<string> AccessScope { get; }
}
@@ -0,0 +1,42 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace VMelnalksnis.NordigenDotNet.Agreements;
/// <summary>Nordigen API client for managing agreements.</summary>
public interface IAgreementClient
{
/// <summary>Gets all agreements.</summary>
/// <param name="pageSize">The number of agreements to get in a single request.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A enumerable which will asynchronously iterate over all available pages.</returns>
IAsyncEnumerable<EndUserAgreement> Get(int pageSize = 100, CancellationToken cancellationToken = default);
/// <summary>Gets the agreement with the specified id.</summary>
/// <param name="id">The id of the agreement to get.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>The agreement with the specified id.</returns>
Task<EndUserAgreement> Get(Guid id, CancellationToken cancellationToken = default);
/// <summary>Creates a new agreement.</summary>
/// <param name="agreementCreation">The agreement to create.</param>
/// <returns>The created agreement.</returns>
Task<EndUserAgreement> Post(EndUserAgreementCreation agreementCreation);
/// <summary>Deletes the specified agreement.</summary>
/// <param name="id">The id of the agreement to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete(Guid id);
/// <summary>Accepts the specified agreement.</summary>
/// <param name="id">The id of the agreement to accept.</param>
/// <param name="agreementAcceptance">Details from the end-user needed to accept the agreement.</param>
/// <returns>The accepted agreement.</returns>
Task<EndUserAgreement> Put(Guid id, EndUserAgreementAcceptance agreementAcceptance);
}
@@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information.
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Agreements;
using VMelnalksnis.NordigenDotNet.Institutions;
using VMelnalksnis.NordigenDotNet.Requisitions;
@@ -14,6 +15,9 @@ public interface INordigenClient
/// <summary>Gets the accounts API client.</summary>
IAccountClient Accounts { get; }
/// <summary>Gets the agreements API client.</summary>
IAgreementClient Agreements { get; }
/// <summary>Gets the institutions API client.</summary>
IInstitutionClient Institutions { get; }
@@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information.
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Agreements;
using VMelnalksnis.NordigenDotNet.Institutions;
using VMelnalksnis.NordigenDotNet.Requisitions;
@@ -13,11 +14,17 @@ public sealed class NordigenClient : INordigenClient
{
/// <summary>Initializes a new instance of the <see cref="NordigenClient"/> class.</summary>
/// <param name="accounts">Accounts API client.</param>
/// <param name="agreementClient">Agreements API client.</param>
/// <param name="institutions">Institutions API client.</param>
/// <param name="requisitions">Requisitions API client.</param>
public NordigenClient(IAccountClient accounts, IInstitutionClient institutions, IRequisitionClient requisitions)
public NordigenClient(
IAccountClient accounts,
IAgreementClient agreementClient,
IInstitutionClient institutions,
IRequisitionClient requisitions)
{
Accounts = accounts;
Agreements = agreementClient;
Institutions = institutions;
Requisitions = requisitions;
}
@@ -25,6 +32,9 @@ public sealed class NordigenClient : INordigenClient
/// <inheritdoc />
public IAccountClient Accounts { get; }
/// <inheritdoc />
public IAgreementClient Agreements { get; }
/// <inheritdoc />
public IInstitutionClient Institutions { get; }
@@ -2,8 +2,10 @@
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
@@ -47,21 +49,76 @@ public sealed class NordigenHttpClient
internal async Task<TResult?> GetAsJson<TResult>(string requestUri, CancellationToken cancellationToken)
{
var client = await GetClient().ConfigureAwait(false);
return await client.GetFromJsonAsync<TResult>(requestUri, _serializerOptions, cancellationToken)
.ConfigureAwait(false);
return await client.GetFromJsonAsync<TResult>(requestUri, _serializerOptions, cancellationToken).ConfigureAwait(false);
}
internal async IAsyncEnumerable<TResult> GetAsJsonPaginated<TResult>(
string requestUri,
[EnumeratorCancellation] CancellationToken cancellationToken)
where TResult : class
{
var next = requestUri;
do
{
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
var paginatedList = await GetAsJson<PaginatedList<TResult>>(next, cancellationToken).ConfigureAwait(false);
if (paginatedList?.Results is null)
{
yield break;
}
foreach (var requisition in paginatedList.Results)
{
yield return requisition;
}
next = paginatedList.Next?.PathAndQuery;
}
while (next is not null);
}
internal async Task<TResult?> PostAsJson<TRequest, TResult>(string requestUri, TRequest request)
{
var client = await GetClient().ConfigureAwait(false);
var response = await client.PostAsJsonAsync(requestUri, request, _serializerOptions).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<TResult>(_serializerOptions).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TResult>(_serializerOptions).ConfigureAwait(false);
}
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(content, null, response.StatusCode);
}
internal async Task<TResult?> PutAsJson<TRequest, TResult>(string requestUri, TRequest request)
{
var client = await GetClient().ConfigureAwait(false);
var response = await client.PutAsJsonAsync(requestUri, request, _serializerOptions).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TResult>(_serializerOptions).ConfigureAwait(false);
}
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(content, null, response.StatusCode);
}
internal async Task Delete(string requestUri)
{
var client = await GetClient().ConfigureAwait(false);
await client.DeleteAsync(requestUri).ConfigureAwait(false);
var response = await client.DeleteAsync(requestUri).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return;
}
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(content, null, response.StatusCode);
}
private async Task<HttpClient> GetClient()
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -23,35 +22,10 @@ public sealed class RequisitionClient : IRequisitionClient
}
/// <inheritdoc />
public async IAsyncEnumerable<Requisition> Get(
int pageSize = 100,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
public IAsyncEnumerable<Requisition> Get(int pageSize = 100, CancellationToken cancellationToken = default)
{
var next = $"{Routes.Requisitions.Uri}?limit={pageSize}&offset=0";
do
{
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
var paginatedResponse = await _nordigenHttpClient
.GetAsJson<PaginatedList<Requisition>>(next, cancellationToken)
.ConfigureAwait(false);
if (paginatedResponse?.Results is null)
{
yield break;
}
foreach (var requisition in paginatedResponse.Results)
{
yield return requisition;
}
next = paginatedResponse.Next?.PathAndQuery;
}
while (next is not null);
var requestUri = $"{Routes.Requisitions.Uri}?limit={pageSize}&offset=0";
return _nordigenHttpClient.GetAsJsonPaginated<Requisition>(requestUri, cancellationToken);
}
/// <inheritdoc />
@@ -8,6 +8,15 @@ namespace VMelnalksnis.NordigenDotNet;
internal static class Routes
{
internal static class Agreements
{
internal const string Uri = "api/v2/agreements/enduser/";
internal static string IdUri(Guid id) => $"{Uri}{id:N}/";
internal static string AcceptUri(Guid id) => $"{IdUri(id)}accept/";
}
internal static class Accounts
{
private const string _uri = "api/v2/accounts/";
@@ -0,0 +1,54 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using VMelnalksnis.NordigenDotNet.Agreements;
namespace VMelnalksnis.NordigenDotNet.Tests.Integration.Agreements;
public sealed class AgreementClientTests : IClassFixture<ServiceProviderFixture>
{
private readonly IAgreementClient _agreementClient;
public AgreementClientTests(ServiceProviderFixture serviceProviderFixture)
{
_agreementClient = serviceProviderFixture.AgreementClient;
}
[Fact]
public async Task Get()
{
var creation = new EndUserAgreementCreation("CITADELE_PARXLV22");
var createdAgreement = await _agreementClient.Post(creation);
var agreements = await _agreementClient.Get().ToListAsync();
agreements
.Should()
.ContainSingle(agreement => agreement.Id == createdAgreement.Id)
.Which.Should()
.BeEquivalentTo(createdAgreement);
var acceptance = new EndUserAgreementAcceptance("NordigenDotNet integration test", "127.0.0.1");
(await FluentActions
.Awaiting(() => _agreementClient.Put(createdAgreement.Id, acceptance))
.Should()
.ThrowExactlyAsync<HttpRequestException>())
.Which.StatusCode.Should()
.Be(HttpStatusCode.Forbidden, "test company cannot create agreements");
await _agreementClient.Delete(createdAgreement.Id);
(await FluentActions
.Awaiting(() => _agreementClient.Get(createdAgreement.Id))
.Should()
.ThrowExactlyAsync<HttpRequestException>())
.Which.StatusCode.Should()
.Be(HttpStatusCode.NotFound);
}
}
@@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using VMelnalksnis.NordigenDotNet.Accounts;
using VMelnalksnis.NordigenDotNet.Agreements;
using VMelnalksnis.NordigenDotNet.DependencyInjection;
using VMelnalksnis.NordigenDotNet.Institutions;
using VMelnalksnis.NordigenDotNet.Requisitions;
@@ -38,6 +39,8 @@ public sealed class ServiceProviderFixture
public IAccountClient AccountClient => _serviceProvider.GetRequiredService<IAccountClient>();
public IAgreementClient AgreementClient => _serviceProvider.GetRequiredService<IAgreementClient>();
public IInstitutionClient InstitutionClient => _serviceProvider.GetRequiredService<IInstitutionClient>();
public IRequisitionClient RequisitionClient => _serviceProvider.GetRequiredService<IRequisitionClient>();