namespace BudgetApp.Tests.Application; using BudgetApp.Application.Services; using BudgetApp.Domain.Interfaces; using BudgetApp.Domain.Models; using Moq; using Xunit; public class LedgerServiceTests { private readonly Mock _mockLedgerRepository; private readonly Mock _mockBudgetRepository; private readonly LedgerService _service; public LedgerServiceTests() { _mockLedgerRepository = new Mock(); _mockBudgetRepository = new Mock(); _service = new LedgerService(_mockLedgerRepository.Object, _mockBudgetRepository.Object); } [Fact] public async Task RecordExpenseAsync_WithValidParameters_CreatesAndSavesEntry() { // Arrange var budget = new BaseBudget("Grocery", new Money(100m, "USD")); var budgetId = budget.Id; var budgetAllocations = new Dictionary { { budgetId, new Money(100m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget }); _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId)) .ReturnsAsync(budget); _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.GetOrCreateAsync()) .ReturnsAsync(new Ledger()); _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny())) .Returns(Task.CompletedTask); // Act var entry = await _service.RecordExpenseAsync( DateTime.Now, "Grocery Shopping", new Money(100m, "USD"), budgetAllocations); // Assert Assert.NotNull(entry); Assert.Equal(EntryType.Expense, entry.Type); Assert.Equal(100m, entry.Amount.Amount); _mockLedgerRepository.Verify(r => r.SaveEntryAsync(It.IsAny()), Times.Once); _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny()), Times.Once); } [Fact] public async Task RecordExpenseAsync_WithAllocationsNotMatchingAmount_ThrowsException() { // Arrange var budgetId = Guid.NewGuid(); var budget = new BaseBudget("Grocery", new Money(100m, "USD")); var budgetAllocations = new Dictionary { { budgetId, new Money(50m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget }); // Act & Assert await Assert.ThrowsAsync(() => _service.RecordExpenseAsync( DateTime.Now, "Grocery Shopping", new Money(100m, "USD"), budgetAllocations)); } [Fact] public async Task RecordExpenseAsync_WithNonExistentBudget_ThrowsException() { // Arrange var budgetId = Guid.NewGuid(); var budgetAllocations = new Dictionary { { budgetId, new Money(100m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List()); // Act & Assert await Assert.ThrowsAsync(() => _service.RecordExpenseAsync( DateTime.Now, "Grocery Shopping", new Money(100m, "USD"), budgetAllocations)); } [Fact] public async Task RecordExpenseAsync_WithMultipleBudgets_UpdatesAllBudgets() { // Arrange var budget1 = new BaseBudget("Grocery", new Money(100m, "USD")); var budget2 = new BaseBudget("Medical", new Money(50m, "USD")); var budgetId1 = budget1.Id; var budgetId2 = budget2.Id; var budgetAllocations = new Dictionary { { budgetId1, new Money(90m, "USD") }, { budgetId2, new Money(10m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget1, budget2 }); _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId1)) .ReturnsAsync(budget1); _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId2)) .ReturnsAsync(budget2); _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.GetOrCreateAsync()) .ReturnsAsync(new Ledger()); _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny())) .Returns(Task.CompletedTask); // Act var entry = await _service.RecordExpenseAsync( DateTime.Now, "Grocery Shopping", new Money(100m, "USD"), budgetAllocations); // Assert Assert.NotNull(entry); Assert.Equal(2, entry.BudgetAllocations.Count); _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny()), Times.Exactly(2)); } [Fact] public async Task RecordIncomeAsync_WithValidParameters_CreatesAndSavesEntry() { // Arrange var budget = new BaseBudget("Grocery", new Money(100m, "USD")); var budgetId = budget.Id; var budgetAllocations = new Dictionary { { budgetId, new Money(1000m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget }); _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId)) .ReturnsAsync(budget); _mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.GetOrCreateAsync()) .ReturnsAsync(new Ledger()); _mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny())) .Returns(Task.CompletedTask); _mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny())) .Returns(Task.CompletedTask); // Act var entry = await _service.RecordIncomeAsync( DateTime.Now, "Salary", new Money(1000m, "USD"), budgetAllocations); // Assert Assert.NotNull(entry); Assert.Equal(EntryType.Income, entry.Type); Assert.Equal(1000m, entry.Amount.Amount); _mockLedgerRepository.Verify(r => r.SaveEntryAsync(It.IsAny()), Times.Once); _mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny()), Times.Once); } [Fact] public async Task RecordIncomeAsync_WithAllocationsNotMatchingAmount_ThrowsException() { // Arrange var budgetId = Guid.NewGuid(); var budget = new BaseBudget("Grocery", new Money(100m, "USD")); var budgetAllocations = new Dictionary { { budgetId, new Money(500m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget }); // Act & Assert await Assert.ThrowsAsync(() => _service.RecordIncomeAsync( DateTime.Now, "Salary", new Money(1000m, "USD"), budgetAllocations)); } [Fact] public async Task GetTotalBudgetBalanceAsync_ReturnsSumOfAllBudgets() { // Arrange var budgets = new List { new BaseBudget("Grocery", new Money(100m, "USD")), new BaseBudget("Medical", new Money(200m, "USD")) }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(budgets); // Act var total = await _service.GetTotalBudgetBalanceAsync("USD"); // Assert Assert.Equal(300m, total.Amount); } [Fact] public async Task RecordExpenseAsync_WithInsufficientBudgetBalance_ThrowsException() { // Arrange var budget = new BaseBudget("Grocery", new Money(50m, "USD")); var budgetId = budget.Id; var budgetAllocations = new Dictionary { { budgetId, new Money(100m, "USD") } }; _mockBudgetRepository.Setup(r => r.GetAllAsync()) .ReturnsAsync(new List { budget }); _mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId)) .ReturnsAsync(budget); // Act & Assert await Assert.ThrowsAsync(() => _service.RecordExpenseAsync( DateTime.Now, "Grocery Shopping", new Money(100m, "USD"), budgetAllocations)); } }