AccountingNext/Schaad.Accounting.Tests/AccountRepositoryTestShould.cs
Claudio Schaad 8926fb1e97 PR G: test project with xxxTestShould / DoThisWhenThat / Shouldly
Add Schaad.Accounting.Tests (xUnit + NSubstitute + Shouldly, net9.0)
wired into Accounting.sln with a direct Reference to
Schaad.Finance.Api.dll so it flows into the test binary.

Naming convention: file/class is <Subject>TestShould, each test method
is DoThisWhenThat. Reads as a sentence:
"ViewService test should sum activa and passiva totals separately when
getting balance view". All assertions use Shouldly (.ShouldBe,
.ShouldBeNull, .ShouldContain, ...) rather than xUnit Assert.*.

38 tests across seven files:

- ViewServiceTestShould: balance math (activa/passiva totals, per-
  account balance from start balance + credits - debits, FX conversion
  to CHF) and bank-transaction auto-matching (booking-rule text,
  value-matching preference, same-accounts-last-month fallback,
  open-transaction filter).
- TransactionRepositoryTestShould: FX round-trip against a temp XML
  directory, unknown-id -> null, and the mutation-on-read regression
  from PR A (a second Get on the same FX transaction used to divide
  by FxRate again).
- FormattingTestShould: Swiss thousands separator, two-decimal
  rounding, culture independence.
- RepositoryCacheTestShould: loader called once, per-key isolation,
  invalidation forces reload, case-insensitive keys.
- AccountRepositoryTestShould: id assignment, in-place update,
  currency defaulting, delete, bank-account lookup, bank-balance
  update, and constructor re-run against an existing file.
- ChartServiceTestShould: honours settingsService.GetYear() without
  mutating it (locks in PR E), account vs sub-class grouping
  heuristic, skips empty accounts.
- FileServiceTestShould: CSV header + running balance for debit and
  credit lines, ordering by BookingDate/ValueDate/Value, no in-place
  value mutation (locks in PR A).

CLAUDE.md gains a `dotnet test` line. IMPROVEMENT_PLAN.md notes that
this landed as three PRs (G, H, I) and was squashed on request.

Also add `*.DotSettings.user` to .gitignore so Rider's per-user
solution settings don't get accidentally staged.

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

112 lines
3.7 KiB
C#

using NSubstitute;
using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models;
using Schaad.Accounting.Repositories;
using Shouldly;
namespace Schaad.Accounting.Tests;
public class AccountRepositoryTestShould : IDisposable
{
private readonly string dbDir;
private readonly ISettingsService settingsService;
private readonly AccountRepository sut;
public AccountRepositoryTestShould()
{
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"));
sut = new AccountRepository(settingsService, new RepositoryCache());
}
public void Dispose()
{
if (Directory.Exists(dbDir))
{
Directory.Delete(dbDir, recursive: true);
}
}
[Fact]
public void AssignNewIdWhenSavingAccountWithoutId()
{
var account = new Account { Number = 1000, Name = "Cash", Currency = "CHF" };
sut.SaveAccount(account);
var stored = sut.GetAccountList().Single();
stored.Id.ShouldNotBeNullOrEmpty();
stored.Name.ShouldBe("Cash");
}
[Fact]
public void UpdateInPlaceWhenSavingAccountWithExistingId()
{
sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" });
var id = sut.GetAccountList().Single().Id;
sut.SaveAccount(new Account { Id = id, Number = 1000, Name = "Cash renamed", Currency = "CHF" });
var accounts = sut.GetAccountList();
accounts.Count.ShouldBe(1);
accounts[0].Name.ShouldBe("Cash renamed");
}
[Fact]
public void DefaultCurrencyToChfWhenSavingAccountWithoutCurrency()
{
sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = null! });
sut.GetAccountList().Single().Currency.ShouldBe("CHF");
}
[Fact]
public void RemoveAccountWhenDeletingById()
{
sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" });
var id = sut.GetAccountList().Single().Id;
sut.DeleteAccount(id);
sut.GetAccountList().ShouldBeEmpty();
}
[Fact]
public void ReturnAccountWhenLookingUpByBankAccountNumber()
{
sut.SaveAccount(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" });
sut.SaveAccount(new Account { Number = 1020, Name = "Savings", Currency = "CHF", BankAccountNumber = "CH02" });
sut.GetAccountForBankAccountNumber("CH01").Name.ShouldBe("Checking");
sut.GetAccountForBankAccountNumber("CH02").Name.ShouldBe("Savings");
sut.GetAccountForBankAccountNumber("CH99").ShouldBeNull();
}
[Fact]
public void UpdateLastBankBalanceWhenSavingBankBalance()
{
sut.SaveAccount(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" });
sut.SaveBankAccountBalance("CH01", 1234.56m);
sut.GetAccountForBankAccountNumber("CH01").LastBankBalance.ShouldBe(1234.56m);
}
[Fact]
public void SurviveConstructorReRunWhenAccountsFileAlreadyExists()
{
sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" });
// Re-construct with the same directory: existing file, no year rollover, no data loss.
var fresh = new AccountRepository(settingsService, new RepositoryCache());
var accounts = fresh.GetAccountList();
accounts.Count.ShouldBe(1);
accounts[0].Name.ShouldBe("Cash");
}
}