129 lines
4.6 KiB
C#
129 lines
4.6 KiB
C#
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<FinanceDbContext>()
|
|
.UseSqlite("Data Source=duempelkas-transfer-tests;Mode=Memory;Cache=Shared")
|
|
.Options;
|
|
|
|
_db = new FinanceDbContext(options);
|
|
_db.Database.OpenConnection();
|
|
_db.Database.EnsureCreated();
|
|
|
|
var dbFactory = new TestDbContextFactory(options);
|
|
|
|
_entryService = new EntryService(dbFactory);
|
|
_balanceQueryService = new BalanceQueryService(dbFactory);
|
|
}
|
|
|
|
[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);
|
|
|
|
_db.ChangeTracker.Clear();
|
|
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<InvalidOperationException>();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_db.Database.CloseConnection();
|
|
_db.Dispose();
|
|
}
|
|
|
|
private sealed class TestDbContextFactory : IDbContextFactory<FinanceDbContext>
|
|
{
|
|
private readonly DbContextOptions<FinanceDbContext> _options;
|
|
|
|
public TestDbContextFactory(DbContextOptions<FinanceDbContext> options)
|
|
{
|
|
_options = options;
|
|
}
|
|
|
|
public FinanceDbContext CreateDbContext() => new(_options);
|
|
|
|
public Task<FinanceDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new FinanceDbContext(_options));
|
|
}
|
|
}
|