AccountingNext/Schaad.Accounting.Tests/TransactionRepositoryTestShould.cs
Claudio Schaad 3a770af016 PR J: log repository save failures via ILogger
BaseRepository takes an ILogger via its constructor and logs the
failing file path and exception in Save's catch block before
rethrowing. Every concrete repository takes ILogger<ThisRepo> and
passes it up so failures are categorised by repository type.

Pulls Microsoft.Extensions.Logging.Abstractions into the Db project.
TransactionRepositoryTestShould and AccountRepositoryTestShould pass
NullLogger<T>.Instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:47:55 +02:00

139 lines
4.7 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.Repositories;
using Shouldly;
namespace Schaad.Accounting.Tests;
public class TransactionRepositoryTestShould : IDisposable
{
private readonly string dbDir;
private readonly ISettingsService settingsService;
private readonly IAccountRepository accountRepo;
private readonly RepositoryCache cache;
private readonly TransactionRepository sut;
public TransactionRepositoryTestShould()
{
dbDir = Path.Combine(Path.GetTempPath(), "AccountingNext-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dbDir);
settingsService = Substitute.For<ISettingsService>();
settingsService.GetDbPath().Returns(dbDir);
settingsService.GetLastYearDbPath().Returns(Path.Combine(dbDir, "no-such-dir"));
accountRepo = Substitute.For<IAccountRepository>();
cache = new RepositoryCache();
sut = new TransactionRepository(settingsService, cache, accountRepo, NullLogger<TransactionRepository>.Instance);
}
public void Dispose()
{
if (Directory.Exists(dbDir))
{
Directory.Delete(dbDir, recursive: true);
}
}
[Fact]
public void PreserveValueWhenRoundTrippingNonFxTransaction()
{
accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" });
accountRepo.GetAccount("groc").Returns(new Account { Id = "groc", Currency = "CHF" });
sut.SaveTransaction(new Transaction
{
OriginAccountId = "chk",
TargetAccountId = "groc",
Value = 42m,
Text = "Groceries",
ValueDate = new DateTime(2026, 5, 10),
BookingDate = new DateTime(2026, 5, 10)
});
var stored = sut.GetTransactionList().Single();
var loaded = sut.GetTransaction(stored.Id);
loaded.ShouldNotBeNull();
loaded.Value.ShouldBe(42m);
loaded.FxRate.ShouldBeNull(); // Non-FX transactions have FxRate cleared
}
[Fact]
public void StoreInChfAndReturnInAccountCurrencyWhenTransactionIsFx()
{
// User enters 100 USD at rate 0.9 CHF/USD -> stored as 90 CHF; read back as 100 USD.
accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" });
accountRepo.GetAccount("usd").Returns(new Account { Id = "usd", Currency = "USD" });
sut.SaveTransaction(new Transaction
{
OriginAccountId = "chk",
TargetAccountId = "usd",
Value = 100m,
FxRate = 0.9m,
Text = "USD transfer",
ValueDate = new DateTime(2026, 5, 10),
BookingDate = new DateTime(2026, 5, 10)
});
var stored = sut.GetTransactionList().Single();
stored.Value.ShouldBe(90m); // stored in CHF
var loaded = sut.GetTransaction(stored.Id);
loaded.ShouldNotBeNull();
loaded.Value.ShouldBe(100m); // display currency
loaded.FxRate.ShouldBe(0.9m);
}
[Fact]
public void ReturnNullWhenGettingUnknownTransactionId()
{
sut.GetTransaction("no-such-id").ShouldBeNull();
}
[Fact]
public void NotMutateStoredValueWhenGettingFxTransactionTwice()
{
accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" });
accountRepo.GetAccount("usd").Returns(new Account { Id = "usd", Currency = "USD" });
sut.SaveTransaction(new Transaction
{
OriginAccountId = "chk",
TargetAccountId = "usd",
Value = 100m,
FxRate = 0.9m,
Text = "USD transfer",
ValueDate = new DateTime(2026, 5, 10),
BookingDate = new DateTime(2026, 5, 10)
});
var stored = sut.GetTransactionList().Single();
var id = stored.Id;
// Get twice; if Get mutated the stored entity, the second call would divide by FxRate again.
var first = sut.GetTransaction(id);
var second = sut.GetTransaction(id);
first.ShouldNotBeNull();
first.Value.ShouldBe(100m);
second.ShouldNotBeNull();
second.Value.ShouldBe(100m);
}
[Fact]
public void RemoveTransactionWhenDeletingById()
{
accountRepo.GetAccount(Arg.Any<string>()).Returns(new Account { Currency = "CHF" });
sut.SaveTransaction(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 1m, Text = "x", ValueDate = DateTime.Today });
var id = sut.GetTransactionList().Single().Id;
sut.DeleteTransaction(id);
sut.GetTransactionList().ShouldBeEmpty();
}
}