diff --git a/tests/Duempelkas.Tests/Duempelkas.Tests.csproj b/tests/Duempelkas.Tests/Duempelkas.Tests.csproj new file mode 100644 index 0000000..19a5933 --- /dev/null +++ b/tests/Duempelkas.Tests/Duempelkas.Tests.csproj @@ -0,0 +1,26 @@ + + + net10.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/tests/Duempelkas.Tests/TransferServiceTests.cs b/tests/Duempelkas.Tests/TransferServiceTests.cs new file mode 100644 index 0000000..82fe310 --- /dev/null +++ b/tests/Duempelkas.Tests/TransferServiceTests.cs @@ -0,0 +1,110 @@ +using Duempelkas.Domain.Entities; +using Duempelkas.Domain.Enums; +using Duempelkas.Infrastructure.Persistence; +using Duempelkas.Infrastructure.Services; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Duempelkas.Tests; + +public class TransferServiceTests : IDisposable +{ + private readonly FinanceDbContext _db; + private readonly EntryService _entryService; + private readonly BalanceQueryService _balanceQueryService; + + public TransferServiceTests() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=:memory:") + .Options; + + _db = new FinanceDbContext(options); + _db.Database.OpenConnection(); + _db.Database.EnsureCreated(); + + _entryService = new EntryService(_db); + _balanceQueryService = new BalanceQueryService(_db); + } + + [Fact] + public async Task CreateTransfer_CreatesLinkedExpenseAndIncome() + { + var accountA = new Account { Name = "Account A" }; + var accountB = new Account { Name = "Account B" }; + _db.Accounts.AddRange(accountA, accountB); + await _db.SaveChangesAsync(); + + await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m); + + var entries = await _db.Entries.ToListAsync(); + entries.Should().HaveCount(2); + + var sourceEntry = entries.Single(e => e.AccountId == accountA.Id); + sourceEntry.Type.Should().Be(EntryType.Expense); + sourceEntry.Amount.Should().Be(100.00m); + sourceEntry.Title.Should().Be("Test Transfer"); + sourceEntry.DisplayId.Should().Be("2026-001"); + + var targetEntry = entries.Single(e => e.AccountId == accountB.Id); + targetEntry.Type.Should().Be(EntryType.Income); + targetEntry.Amount.Should().Be(100.00m); + targetEntry.DisplayId.Should().Be("2026-002"); + + var links = await _db.TransferLinks.ToListAsync(); + links.Should().HaveCount(1); + links[0].SourceEntryId.Should().Be(sourceEntry.Id); + links[0].TargetEntryId.Should().Be(targetEntry.Id); + + var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); + balanceA.TotalBalance.Should().Be(-100.00m); + + var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id); + balanceB.TotalBalance.Should().Be(100.00m); + } + + [Fact] + public async Task DeleteTransfer_SoftDeletesBothSides() + { + var accountA = new Account { Name = "Account A" }; + var accountB = new Account { Name = "Account B" }; + _db.Accounts.AddRange(accountA, accountB); + await _db.SaveChangesAsync(); + + await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m); + var sourceEntry = await _db.Entries.FirstAsync(e => e.AccountId == accountA.Id); + + await _entryService.DeleteEntryAsync(sourceEntry.Id); + + var entries = await _db.Entries.ToListAsync(); + entries.Should().HaveCount(2); + entries.Should().AllSatisfy(e => e.IsDeleted.Should().BeTrue()); + + var links = await _db.TransferLinks.ToListAsync(); + links.Should().HaveCount(1); + + var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); + balanceA.TotalBalance.Should().Be(0m); + + var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id); + balanceB.TotalBalance.Should().Be(0m); + } + + [Fact] + public async Task CreateTransfer_SameAccount_Throws() + { + var account = new Account { Name = "Account A" }; + _db.Accounts.Add(account); + await _db.SaveChangesAsync(); + + var act = () => _entryService.CreateTransferAsync(account.Id, account.Id, DateTime.Today, "Bad Transfer", 50m); + await act.Should().ThrowAsync(); + } + + public void Dispose() + { + _db.Database.CloseConnection(); + _db.Dispose(); + } +} diff --git a/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs b/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs new file mode 100644 index 0000000..9af8946 --- /dev/null +++ b/tests/Duempelkas.Tests/YearlyStatementCalculationTests.cs @@ -0,0 +1,109 @@ +using Duempelkas.Domain.Entities; +using Duempelkas.Domain.Enums; +using Duempelkas.Infrastructure.Persistence; +using Duempelkas.Infrastructure.Services; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Duempelkas.Tests; + +public class BalanceCalculationTests : IDisposable +{ + private readonly FinanceDbContext _db; + private readonly BalanceQueryService _balanceQueryService; + private readonly EntryService _entryService; + + public BalanceCalculationTests() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=:memory:") + .Options; + + _db = new FinanceDbContext(options); + _db.Database.OpenConnection(); + _db.Database.EnsureCreated(); + + _balanceQueryService = new BalanceQueryService(_db); + _entryService = new EntryService(_db); + } + + [Fact] + public async Task AccountBalance_CalculatesCorrectTotals() + { + var account = new Account { Name = "Test Account", CarryoverBalance = 500.00m }; + _db.Accounts.Add(account); + await _db.SaveChangesAsync(); + + _db.Entries.AddRange( + new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 1, 15), Title = "Gehalt", Amount = 800.00m }, + new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Income, Date = new DateTime(2026, 2, 15), Title = "Bonus", Amount = 400.00m }, + new Entry { AccountId = account.Id, DisplayId = "2026-003", Type = EntryType.Expense, Date = new DateTime(2026, 1, 20), Title = "Miete", Amount = 200.00m }, + new Entry { AccountId = account.Id, DisplayId = "2026-004", Type = EntryType.Expense, Date = new DateTime(2026, 2, 20), Title = "Lebensmittel", Amount = 250.00m } + ); + await _db.SaveChangesAsync(); + + var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id); + + balance.CarryoverBalance.Should().Be(500.00m); + balance.TotalIncome.Should().Be(1200.00m); + balance.TotalExpense.Should().Be(450.00m); + balance.TotalBalance.Should().Be(1250.00m); + } + + [Fact] + public async Task AccountBalance_IncludesTransfersCorrectly() + { + var accountA = new Account { Name = "Account A", CarryoverBalance = 500.00m }; + var accountB = new Account { Name = "Account B" }; + _db.Accounts.AddRange(accountA, accountB); + await _db.SaveChangesAsync(); + + _db.Entries.Add(new Entry + { + AccountId = accountA.Id, + DisplayId = "2026-001", + Type = EntryType.Income, + Date = new DateTime(2026, 1, 10), + Title = "Gehalt", + Amount = 1000.00m + }); + await _db.SaveChangesAsync(); + + await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 2, 1), "Umbuchung nach B", 300.00m); + + var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id); + + balanceA.CarryoverBalance.Should().Be(500.00m); + balanceA.TotalIncome.Should().Be(1000.00m); + balanceA.TotalExpense.Should().Be(300.00m); + balanceA.TotalBalance.Should().Be(1200.00m); + } + + [Fact] + public async Task AccountBalance_WithCarryover() + { + var account = new Account { Name = "Konto mit Übertrag", CarryoverBalance = 1000.00m }; + _db.Accounts.Add(account); + await _db.SaveChangesAsync(); + + _db.Entries.AddRange( + new Entry { AccountId = account.Id, DisplayId = "2025-001", Type = EntryType.Income, Date = new DateTime(2025, 6, 1), Title = "Einnahme 2025", Amount = 500.00m }, + new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 6, 1), Title = "Einnahme 2026", Amount = 300.00m }, + new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Expense, Date = new DateTime(2026, 7, 1), Title = "Ausgabe 2026", Amount = 150.00m } + ); + await _db.SaveChangesAsync(); + + var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id); + + // 1000 + 500 + 300 - 150 = 1650 + balance.TotalBalance.Should().Be(1650.00m); + balance.CarryoverBalance.Should().Be(1000.00m); + } + + public void Dispose() + { + _db.Database.CloseConnection(); + _db.Dispose(); + } +}