Skip to content

Commit

Permalink
Fix account details model (#17)
Browse files Browse the repository at this point in the history
* Change Singleton to Transient

* Update account details model
Change to WhenAll for fetching account details

* Remove uneccessary properties from bank models
Add log text that explains which field went wrong when serializing

* Refactor and enhance banking service and tests

- Update `BankClient_v2.cs` to adjust JSON properties and make `CreditDebitIndicator` nullable.
- Add `AccountExtensions.cs` with error logging method.
- Add `HasErrors` property to `AccountV2` in `BankResponse.cs`.
- Enhance `GetAccountDetailsV2` in `BankService.cs` for better error handling and logging.
- Update `Altinn.Dan.Plugin.Banking.Test.csproj` to target `net8.0`, add `FakeItEasy`, and reference `Altinn.Dan.Plugin.Banking`.
- Add `FakeableHttpMessageHandler.cs` for handling HTTP requests in tests and generating certificates.
- Add `BankServiceTests.cs` with comprehensive unit tests for `BankService`.
- Remove obsolete `UnitTest1.cs`.
  • Loading branch information
DanRJ authored Feb 4, 2025
1 parent 9bbdff9 commit 6c20cd9
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 1,287 deletions.
1,166 changes: 30 additions & 1,136 deletions src/Altinn.Dan.Plugin.Banking/Clients/V2/BankClient_v2.cs

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions src/Altinn.Dan.Plugin.Banking/Extensions/AccountExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Altinn.Dan.Plugin.Banking.Clients.V2;
using Altinn.Dan.Plugin.Banking.Services;
using Microsoft.Extensions.Logging;
using System;

namespace Altinn.Dan.Plugin.Banking.Extensions
{
public static class AccountExtensions
{
public static void LogGetAccountByIdError(
this Account account,
ILogger logger,
Exception e,
BankConfig bank,
Guid accountInfoRequestId)
{
string correlationId = null, innerExceptionMsg = null;
if (e is ApiException k)
{
correlationId = k.CorrelationId;
innerExceptionMsg = k.InnerException?.Message;
}

logger.LogError("GetAccountById failed while processing account {Account} for {Bank} ({OrgNo}) for {Subject}, error {Error}, accountInfoRequestId: {AccountInfoRequestId}, CorrelationId: {CorrelationId}, source: {source}, innerExceptionMessage: {innerExceptionMessage}",
account.AccountReference,
bank.Name,
bank.OrgNo,
account?.PrimaryOwner?.Identifier?.Value[..6],
e.Message,
accountInfoRequestId,
correlationId,
e.Source,
innerExceptionMsg);
}
}
}
1 change: 1 addition & 0 deletions src/Altinn.Dan.Plugin.Banking/Models/BankResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class Account

public class AccountV2
{
public bool HasErrors { get; set; }
public string AccountNumber { get; set; } // Seperate property by now. Copy of AccountDetail.AccountIdentifier
public AccountDetailV2 AccountDetail { get; set; } // Not mapped to internal by now
public ICollection<TransactionV2> Transactions { get; set; } // Not mapped to internal by now
Expand Down
228 changes: 91 additions & 137 deletions src/Altinn.Dan.Plugin.Banking/Services/BankService.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
Expand All @@ -17,4 +18,8 @@
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Altinn.Dan.Plugin.Banking\Altinn.Dan.Plugin.Banking.csproj" />
</ItemGroup>

</Project>
39 changes: 39 additions & 0 deletions test/Altinn.Dan.Plugin.Banking.Test/FakeableHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

namespace Altinn.Dan.Plugin.Banking.Test
{
public abstract class FakeableHttpMessageHandler : HttpMessageHandler
{
public abstract Task<HttpResponseMessage> FakeSendAsync(
HttpRequestMessage request, CancellationToken cancellationToken);

// sealed so FakeItEasy won't intercept calls to this method
protected sealed override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
=> this.FakeSendAsync(request, cancellationToken);
}

public static class CertificateGenerator
{
public static X509Certificate2 GenerateSelfSignedCertificate()
{
string subjectName = "Self-Signed-Cert-Example";
using var rsa = RSA.Create(2048);
var certRequest = new CertificateRequest($"CN={subjectName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

// Add extensions to the request (just as an example)
// Add keyUsage
certRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true));
X509Certificate2 generatedCert = certRequest.CreateSelfSigned(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddYears(10)); // generate the cert and sign!

X509Certificate2 pfxGeneratedCert = new X509Certificate2(generatedCert.Export(X509ContentType.Pfx)); // has to be turned into pfx or Windows at least throws a security credentials not found during sslStream.connectAsClient or HttpClient request...

return pfxGeneratedCert;
}
}
}
267 changes: 267 additions & 0 deletions test/Altinn.Dan.Plugin.Banking.Test/Services/BankServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
using Altinn.ApiClients.Maskinporten.Interfaces;
using Altinn.ApiClients.Maskinporten.Models;
using Altinn.Dan.Plugin.Banking.Clients.V2;
using Altinn.Dan.Plugin.Banking.Config;
using Altinn.Dan.Plugin.Banking.Services;
using FakeItEasy;
using Jose;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Altinn.Dan.Plugin.Banking.Test.Services
{
[TestClass]
public class BankServiceTests
{
private readonly X509Certificate2 _certificate;
private readonly IOptions<ApplicationSettings> _fakeOptions;
private readonly FakeableHttpMessageHandler _handler;
private readonly HttpClient _client;
private readonly ILoggerFactory _fakeLogger;
private readonly IMaskinportenService _fakeMpService;

public BankServiceTests()
{
_fakeLogger = A.Fake<ILoggerFactory>();
_fakeMpService = A.Fake<IMaskinportenService>();
_fakeOptions = A.Fake<IOptions<ApplicationSettings>>();
_handler = A.Fake<FakeableHttpMessageHandler>();
_client = new HttpClient(_handler)
{
BaseAddress = new Uri("http://test.com")
};

A.CallTo(() => _fakeMpService.GetToken(A<string>._, A<string>._, A<string>._, A<string>._, A<string>._, A<string>._, A<bool>._)).Returns(new TokenResponse { AccessToken = "321" });
_certificate = CertificateGenerator.GenerateSelfSignedCertificate();
A.CallTo(() => _fakeOptions.Value).Returns(new ApplicationSettings { Jwk = "321", ClientId = "54345", BankScope = "somescope", OedDecryptCert = _certificate });
}

[TestMethod]
public async Task GetAccounts_Ok_Accounts()
{
// Arrange
var nin = "12345678901";
var fromDate = DateTime.Now.AddMonths(-1);
var toDate = DateTime.Now;
var bankName = "bank1";
var orgNumber = "789";
var bankList = GetDefaultBankConfig(bankName, orgNumber);

var accounts = GetDefaultAccounts(bankName, orgNumber);
var account1Details = GetAccountDetails(accounts.Accounts1.ElementAt(0));
var account2Details = GetAccountDetails(accounts.Accounts1.ElementAt(1));
FakeGetAccounts(accounts);
FakeGetAccountDetails(account1Details);
FakeGetAccountDetails(account2Details);

var bankService = new BankService(_fakeLogger, _fakeMpService, _fakeOptions);

// Act
var result = await bankService.GetAccounts(nin, bankList, fromDate, toDate, Guid.NewGuid(), false);

// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.BankAccounts.Count);
Assert.IsFalse(result.BankAccounts.Single().HasErrors);
Assert.AreEqual(2, result.BankAccounts.Single().Accounts.Count);
}

[TestMethod]
public async Task GetAccounts_InternalServerError_BankHasErrors()
{
// Arrange
var nin = "12345678901";
var fromDate = DateTime.Now.AddMonths(-1);
var toDate = DateTime.Now;
var bankName = "bank1";
var orgNumber = "789";
var bankList = GetDefaultBankConfig(bankName, orgNumber);

var accounts = GetDefaultAccounts(bankName, orgNumber);
FakeGetAccounts(accounts, HttpStatusCode.InternalServerError);

var bankService = new BankService(_fakeLogger, _fakeMpService, _fakeOptions);

// Act
var result = await bankService.GetAccounts(nin, bankList, fromDate, toDate, Guid.NewGuid(), false);

// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.BankAccounts.Count);
Assert.IsTrue(result.BankAccounts.Single().HasErrors);
Assert.AreEqual(0, result.BankAccounts.Single().Accounts.Count);
Assert.AreEqual(bankName, result.BankAccounts.Single().BankName);
}

[TestMethod]
public async Task GetAccounts_GetAccountDetailsInternalServerError_AccountHasErrors()
{
// Arrange
var nin = "12345678901";
var fromDate = DateTime.Now.AddMonths(-1);
var toDate = DateTime.Now;
var bankName = "bank1";
var orgNumber = "789";
var bankList = GetDefaultBankConfig(bankName, orgNumber);

var accounts = GetDefaultAccounts(bankName, orgNumber);
accounts.Accounts1.Add(GetAccount(bankName, orgNumber, "3"));
var account1Details = GetAccountDetails(accounts.Accounts1.ElementAt(0));
var account2Details = GetAccountDetails(accounts.Accounts1.ElementAt(1));
var account3Details = GetAccountDetails(accounts.Accounts1.ElementAt(2));
FakeGetAccounts(accounts);
FakeGetAccountDetails(account1Details);
FakeGetAccountDetails(account2Details, HttpStatusCode.InternalServerError);
FakeGetAccountDetails(account3Details, HttpStatusCode.InternalServerError);

var bankService = new BankService(_fakeLogger, _fakeMpService, _fakeOptions);

// Act
var result = await bankService.GetAccounts(nin, bankList, fromDate, toDate, Guid.NewGuid(), false);

// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.BankAccounts.Count);
Assert.IsFalse(result.BankAccounts.Single().HasErrors);
Assert.AreEqual(2, result.BankAccounts.Single().Accounts.Count);
Assert.IsTrue(result.BankAccounts.Single().Accounts.Any(x => x.HasErrors));
}

private Dictionary<string, BankConfig> GetDefaultBankConfig(string bankName, string orgNumber)
{
return new Dictionary<string, BankConfig>
{
{
orgNumber,
new BankConfig
{
Name = bankName,
Client = _client,
MaskinportenEnv = "test1",
BankAudience = "someaudience"
}
}
};
}

private void FakeGetAccountDetails(AccountDetails accountDetails, HttpStatusCode httpStatusCode = HttpStatusCode.OK)
{
A.CallTo(() => _handler.FakeSendAsync(A<HttpRequestMessage>.That.Matches(x => x.RequestUri != null && x.RequestUri.AbsoluteUri.Contains($"accounts/{accountDetails.Account.AccountReference}")), A<CancellationToken>._))
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = httpStatusCode,
Content = new StringContent(JWT.Encode(JsonConvert.SerializeObject(accountDetails), _certificate.GetRSAPublicKey(), JweAlgorithm.RSA_OAEP_256, JweEncryption.A128CBC_HS256), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))
}));
}

private void FakeGetAccounts(Accounts accounts, HttpStatusCode httpStatusCode = HttpStatusCode.OK)
{
A.CallTo(() => _handler.FakeSendAsync(A<HttpRequestMessage>._, A<CancellationToken>._))
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = httpStatusCode,
Content = new StringContent(JWT.Encode(JsonConvert.SerializeObject(accounts), _certificate.GetRSAPublicKey(), JweAlgorithm.RSA_OAEP_256, JweEncryption.A128CBC_HS256), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))
}))
.NumberOfTimes(1);
}

private static AccountDetails GetAccountDetails(Account account)
{
return new AccountDetails
{
ResponseDetails = new ResponseDetails
{
Message = "OK",
Status = ResponseDetailsStatus.Complete,
},
Account = new AccountDetail
{
AccountIdentifier = account.AccountIdentifier,
AccountReference = account.AccountReference,
PrimaryOwner = account.PrimaryOwner,
Status = account.Status,
Servicer = account.Servicer,
Type = account.Type,
Balances = new List<Balance>
{
new Balance
{
CreditDebitIndicator = CreditOrDebit.Credit,
CreditLineAmount = 100,
CreditLineIncluded = true,
Amount = 100,
Currency = "NOK",
Type = BalanceType.AvailableBalance
},
new Balance
{
CreditDebitIndicator = CreditOrDebit.Credit,
CreditLineAmount = 100,
CreditLineIncluded = true,
Amount = 100,
Currency = "NOK",
Type = BalanceType.AvailableBalance
}
}
},
};
}

private static Accounts GetDefaultAccounts(string bankName, string orgNumber)
{
return new Accounts
{
ResponseDetails = new ResponseDetails
{
Message = "OK",
Status = ResponseDetailsStatus.Complete
},
Accounts1 =
[
GetAccount(bankName, orgNumber, "1"),
GetAccount(bankName, orgNumber, "2"),
]
};
}

private static Account GetAccount(string bankName, string orgNumber, string accRef)
{
return new Account
{
AccountReference = accRef,
AccountIdentifier = "asdasdasd",
Type = AccountType.LoanAccount,
Status = AccountStatus.Enabled,
Servicer = new FinancialInstitution
{
Identifier = new Identifier
{
Type = IdentifierType.NationalIdentityNumber,
Value = orgNumber
},
Name = bankName
},
PrimaryOwner = new AccountRole
{
Identifier = new Identifier
{
Type = IdentifierType.NationalIdentityNumber,
Value = "12345678910"
}
}
};
}
}
}
13 changes: 0 additions & 13 deletions test/Altinn.Dan.Plugin.Banking.Test/UnitTest1.cs

This file was deleted.

0 comments on commit 6c20cd9

Please sign in to comment.