Initial commit

This commit is contained in:
Ryan 2026-01-03 10:29:03 -06:00
parent 3f854cefc6
commit dcf2ad67bb
283 changed files with 12235 additions and 1 deletions

1
.gitignore vendored
View File

@ -11,4 +11,3 @@
# Built Visual Studio Code Extensions
*.vsix

View File

@ -0,0 +1,153 @@
namespace BudgetApp.Tests.Application;
using BudgetApp.Application.Services;
using BudgetApp.Domain.Interfaces;
using BudgetApp.Domain.Models;
using Moq;
using Xunit;
public class BudgetServiceTests
{
private readonly Mock<IBudgetRepository> _mockRepository;
private readonly BudgetService _service;
public BudgetServiceTests()
{
_mockRepository = new Mock<IBudgetRepository>();
_service = new BudgetService(_mockRepository.Object);
}
[Fact]
public async Task CreateBaseBudgetAsync_WithValidParameters_CreatesAndSavesBudget()
{
// Arrange
var name = "Grocery";
var initialBalance = new Money(100m, "USD");
_mockRepository.Setup(r => r.SaveAsync(It.IsAny<BaseBudget>()))
.Returns(Task.CompletedTask);
// Act
var budget = await _service.CreateBaseBudgetAsync(name, initialBalance);
// Assert
Assert.NotNull(budget);
Assert.Equal(name, budget.Name);
Assert.Equal(100m, budget.Balance.Amount);
_mockRepository.Verify(r => r.SaveAsync(It.IsAny<BaseBudget>()), Times.Once);
}
[Fact]
public async Task CreateBaseBudgetAsync_WithNullName_ThrowsException()
{
// Arrange
var initialBalance = new Money(100m, "USD");
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() =>
_service.CreateBaseBudgetAsync(null!, initialBalance));
}
[Fact]
public async Task CreateGoalBudgetAsync_WithValidParameters_CreatesAndSavesBudget()
{
// Arrange
var name = "Car Purchase";
var initialBalance = new Money(0m, "USD");
var goalAmount = new Money(30000m, "USD");
var targetDate = DateTime.Now.AddYears(3);
var periodicContribution = new Money(1000m, "USD");
var contributionPeriod = TimeSpan.FromDays(30);
_mockRepository.Setup(r => r.SaveAsync(It.IsAny<GoalBudget>()))
.Returns(Task.CompletedTask);
// Act
var budget = await _service.CreateGoalBudgetAsync(
name, initialBalance, goalAmount, targetDate, periodicContribution, contributionPeriod);
// Assert
Assert.NotNull(budget);
Assert.Equal(name, budget.Name);
Assert.Equal(30000m, budget.GoalAmount.Amount);
_mockRepository.Verify(r => r.SaveAsync(It.IsAny<GoalBudget>()), Times.Once);
}
[Fact]
public async Task GetBudgetByIdAsync_ReturnsBudget()
{
// Arrange
var budgetId = Guid.NewGuid();
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
_mockRepository.Setup(r => r.GetByIdAsync(budgetId))
.ReturnsAsync(budget);
// Act
var result = await _service.GetBudgetByIdAsync(budgetId);
// Assert
Assert.NotNull(result);
Assert.Equal(budget.Id, result.Id);
}
[Fact]
public async Task GetAllBudgetsAsync_ReturnsAllBudgets()
{
// Arrange
var budgets = new List<Budget>
{
new BaseBudget("Grocery", new Money(100m, "USD")),
new BaseBudget("Medical", new Money(200m, "USD"))
};
_mockRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(budgets);
// Act
var result = await _service.GetAllBudgetsAsync();
// Assert
Assert.Equal(2, result.Count());
}
[Fact]
public async Task GetBudgetBalanceAsync_ReturnsBalance()
{
// Arrange
var budgetId = Guid.NewGuid();
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
_mockRepository.Setup(r => r.GetByIdAsync(budgetId))
.ReturnsAsync(budget);
// Act
var balance = await _service.GetBudgetBalanceAsync(budgetId);
// Assert
Assert.NotNull(balance);
Assert.Equal(100m, balance.Amount);
}
[Fact]
public async Task GetTotalBudgetBalanceAsync_ReturnsSumOfAllBudgets()
{
// Arrange
var budgets = new List<Budget>
{
new BaseBudget("Grocery", new Money(100m, "USD")),
new BaseBudget("Medical", new Money(200m, "USD"))
};
_mockRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(budgets);
// Act
var total = await _service.GetTotalBudgetBalanceAsync("USD");
// Assert
Assert.Equal(300m, total.Amount);
}
}

View File

@ -0,0 +1,255 @@
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<ILedgerRepository> _mockLedgerRepository;
private readonly Mock<IBudgetRepository> _mockBudgetRepository;
private readonly LedgerService _service;
public LedgerServiceTests()
{
_mockLedgerRepository = new Mock<ILedgerRepository>();
_mockBudgetRepository = new Mock<IBudgetRepository>();
_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<Guid, Money>
{
{ budgetId, new Money(100m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { budget });
_mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
.ReturnsAsync(budget);
_mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny<Budget>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
.ReturnsAsync(new Ledger());
_mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
.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<LedgerEntry>()), Times.Once);
_mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny<Budget>()), 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<Guid, Money>
{
{ budgetId, new Money(50m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { budget });
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_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<Guid, Money>
{
{ budgetId, new Money(100m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget>());
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_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<Guid, Money>
{
{ budgetId1, new Money(90m, "USD") },
{ budgetId2, new Money(10m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { 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<Budget>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
.ReturnsAsync(new Ledger());
_mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
.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<Budget>()), 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<Guid, Money>
{
{ budgetId, new Money(1000m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { budget });
_mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
.ReturnsAsync(budget);
_mockBudgetRepository.Setup(r => r.SaveAsync(It.IsAny<Budget>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.GetOrCreateAsync())
.ReturnsAsync(new Ledger());
_mockLedgerRepository.Setup(r => r.SaveAsync(It.IsAny<Ledger>()))
.Returns(Task.CompletedTask);
_mockLedgerRepository.Setup(r => r.SaveEntryAsync(It.IsAny<LedgerEntry>()))
.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<LedgerEntry>()), Times.Once);
_mockBudgetRepository.Verify(r => r.SaveAsync(It.IsAny<Budget>()), 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<Guid, Money>
{
{ budgetId, new Money(500m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { budget });
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_service.RecordIncomeAsync(
DateTime.Now,
"Salary",
new Money(1000m, "USD"),
budgetAllocations));
}
[Fact]
public async Task GetTotalBudgetBalanceAsync_ReturnsSumOfAllBudgets()
{
// Arrange
var budgets = new List<Budget>
{
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<Guid, Money>
{
{ budgetId, new Money(100m, "USD") }
};
_mockBudgetRepository.Setup(r => r.GetAllAsync())
.ReturnsAsync(new List<Budget> { budget });
_mockBudgetRepository.Setup(r => r.GetByIdAsync(budgetId))
.ReturnsAsync(budget);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_service.RecordExpenseAsync(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
budgetAllocations));
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BudgetApp\BudgetApp.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,90 @@
namespace BudgetApp.Tests.Domain;
using BudgetApp.Domain.Models;
using Xunit;
public class BaseBudgetTests
{
[Fact]
public void Constructor_WithValidParameters_CreatesBudget()
{
// Arrange & Act
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Assert
Assert.Equal("Grocery", budget.Name);
Assert.Equal(100m, budget.Balance.Amount);
Assert.NotEqual(Guid.Empty, budget.Id);
}
[Fact]
public void AddMoney_IncreasesBalance()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act
budget.AddMoney(new Money(50m, "USD"));
// Assert
Assert.Equal(150m, budget.Balance.Amount);
}
[Fact]
public void RemoveMoney_DecreasesBalance()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act
budget.RemoveMoney(new Money(50m, "USD"));
// Assert
Assert.Equal(50m, budget.Balance.Amount);
}
[Fact]
public void RemoveMoney_ResultingInNegative_ThrowsException()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act & Assert
Assert.Throws<InvalidOperationException>(() => budget.RemoveMoney(new Money(150m, "USD")));
}
[Fact]
public void RemoveMoney_WithDifferentCurrency_ThrowsException()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act & Assert
Assert.Throws<InvalidOperationException>(() => budget.RemoveMoney(new Money(50m, "EUR")));
}
[Fact]
public void AddMoney_WithDifferentCurrency_ThrowsException()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act & Assert
Assert.Throws<InvalidOperationException>(() => budget.AddMoney(new Money(50m, "EUR")));
}
[Fact]
public void Balance_NeverNegative()
{
// Arrange
var budget = new BaseBudget("Grocery", new Money(100m, "USD"));
// Act
budget.RemoveMoney(new Money(100m, "USD"));
// Assert
Assert.Equal(0m, budget.Balance.Amount);
}
}

View File

@ -0,0 +1,158 @@
namespace BudgetApp.Tests.Domain;
using BudgetApp.Domain.Models;
using Xunit;
public class GoalBudgetTests
{
[Fact]
public void Constructor_WithValidParameters_CreatesGoalBudget()
{
// Arrange & Act
var targetDate = DateTime.Now.AddYears(3);
var budget = new GoalBudget(
"Car Purchase",
new Money(0m, "USD"),
new Money(30000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Assert
Assert.Equal("Car Purchase", budget.Name);
Assert.Equal(30000m, budget.GoalAmount.Amount);
Assert.Equal(targetDate, budget.TargetDate);
Assert.Equal(1000m, budget.PeriodicContribution.Amount);
}
[Fact]
public void Constructor_WithPastTargetDate_ThrowsException()
{
// Arrange, Act & Assert
var pastDate = DateTime.Now.AddDays(-1);
Assert.Throws<ArgumentException>(() => new GoalBudget(
"Test",
new Money(0m, "USD"),
new Money(10000m, "USD"),
pastDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30)));
}
[Fact]
public void Constructor_WithZeroGoalAmount_ThrowsException()
{
// Arrange, Act & Assert
var targetDate = DateTime.Now.AddYears(1);
Assert.Throws<ArgumentException>(() => new GoalBudget(
"Test",
new Money(0m, "USD"),
new Money(0m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30)));
}
[Fact]
public void IsGoalAchievable_WithSufficientContributions_ReturnsTrue()
{
// Arrange
var targetDate = DateTime.Now.AddYears(3);
var budget = new GoalBudget(
"Car Purchase",
new Money(0m, "USD"),
new Money(36000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Act
var isAchievable = budget.IsGoalAchievable(DateTime.Now);
// Assert
Assert.True(isAchievable);
}
[Fact]
public void IsGoalAchievable_WithInsufficientContributions_ReturnsFalse()
{
// Arrange
var targetDate = DateTime.Now.AddMonths(1);
var budget = new GoalBudget(
"Car Purchase",
new Money(0m, "USD"),
new Money(100000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Act
var isAchievable = budget.IsGoalAchievable(DateTime.Now);
// Assert
Assert.False(isAchievable);
}
[Fact]
public void CalculateRequiredContributionRate_WithSufficientTime_ReturnsCorrectRate()
{
// Arrange
var targetDate = DateTime.Now.AddYears(3);
var budget = new GoalBudget(
"Car Purchase",
new Money(0m, "USD"),
new Money(36000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Act
var requiredRate = budget.CalculateRequiredContributionRate(DateTime.Now);
// Assert
Assert.True(requiredRate.Amount > 0);
Assert.True(requiredRate.Amount <= 1000m); // Should be achievable
}
[Fact]
public void CalculateRequiredContributionRate_WithCurrentBalanceAtGoal_ReturnsZero()
{
// Arrange
var targetDate = DateTime.Now.AddYears(1);
var budget = new GoalBudget(
"Car Purchase",
new Money(10000m, "USD"),
new Money(10000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Act
var requiredRate = budget.CalculateRequiredContributionRate(DateTime.Now);
// Assert
Assert.Equal(0m, requiredRate.Amount);
}
[Fact]
public void IsGoalAchievable_WithBalanceExceedingGoal_ReturnsTrue()
{
// Arrange
var targetDate = DateTime.Now.AddYears(1);
var budget = new GoalBudget(
"Car Purchase",
new Money(15000m, "USD"),
new Money(10000m, "USD"),
targetDate,
new Money(1000m, "USD"),
TimeSpan.FromDays(30));
// Act
var isAchievable = budget.IsGoalAchievable(DateTime.Now);
// Assert
Assert.True(isAchievable);
}
}

View File

@ -0,0 +1,148 @@
namespace BudgetApp.Tests.Domain;
using BudgetApp.Domain.Models;
using Xunit;
public class LedgerEntryTests
{
[Fact]
public void Constructor_WithValidExpense_CreatesEntry()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(100m, "USD") }
};
// Act
var entry = new LedgerEntry(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations);
// Assert
Assert.Equal("Grocery Shopping", entry.Description);
Assert.Equal(100m, entry.Amount.Amount);
Assert.Equal(EntryType.Expense, entry.Type);
Assert.Single(entry.BudgetAllocations);
}
[Fact]
public void Constructor_WithValidIncome_CreatesEntry()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(1000m, "USD") }
};
// Act
var entry = new LedgerEntry(
DateTime.Now,
"Salary",
new Money(1000m, "USD"),
EntryType.Income,
budgetAllocations);
// Assert
Assert.Equal("Salary", entry.Description);
Assert.Equal(1000m, entry.Amount.Amount);
Assert.Equal(EntryType.Income, entry.Type);
}
[Fact]
public void Validate_WithExpenseAllocationsNotMatchingAmount_ThrowsException()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(50m, "USD") }
};
// Act & Assert
Assert.Throws<InvalidOperationException>(() => new LedgerEntry(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations));
}
[Fact]
public void Validate_WithIncomeAllocationsNotMatchingAmount_ThrowsException()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(500m, "USD") }
};
// Act & Assert
Assert.Throws<InvalidOperationException>(() => new LedgerEntry(
DateTime.Now,
"Salary",
new Money(1000m, "USD"),
EntryType.Income,
budgetAllocations));
}
[Fact]
public void Validate_WithMultipleBudgetAllocationsSummingToAmount_IsValid()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(90m, "USD") },
{ Guid.NewGuid(), new Money(10m, "USD") }
};
// Act
var entry = new LedgerEntry(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations);
// Assert
Assert.Equal(2, entry.BudgetAllocations.Count);
Assert.Equal(100m, entry.Amount.Amount);
}
[Fact]
public void Validate_WithEmptyBudgetAllocations_ThrowsException()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => new LedgerEntry(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations));
}
[Fact]
public void Validate_WithNullDescription_ThrowsException()
{
// Arrange
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(100m, "USD") }
};
// Act & Assert
Assert.Throws<ArgumentException>(() => new LedgerEntry(
DateTime.Now,
null!,
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations));
}
}

View File

@ -0,0 +1,152 @@
namespace BudgetApp.Tests.Domain;
using BudgetApp.Domain.Models;
using Xunit;
public class LedgerTests
{
[Fact]
public void Constructor_CreatesEmptyLedger()
{
// Arrange & Act
var ledger = new Ledger();
// Assert
Assert.Empty(ledger.Entries);
}
[Fact]
public void AddEntry_WithValidEntry_AddsToLedger()
{
// Arrange
var ledger = new Ledger();
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(100m, "USD") }
};
var entry = new LedgerEntry(
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations);
// Act
ledger.AddEntry(entry);
// Assert
Assert.Single(ledger.Entries);
Assert.Equal(entry, ledger.Entries.First());
}
[Fact]
public void AddEntry_WithInvalidEntry_ThrowsException()
{
// Arrange
var ledger = new Ledger();
var budgetAllocations = new Dictionary<Guid, Money>
{
{ Guid.NewGuid(), new Money(50m, "USD") }
};
// Act & Assert - The exception is thrown during construction, not when adding
Assert.Throws<InvalidOperationException>(() => new LedgerEntry(
Guid.NewGuid(),
DateTime.Now,
"Grocery Shopping",
new Money(100m, "USD"),
EntryType.Expense,
budgetAllocations));
}
[Fact]
public void GetEntriesByType_ReturnsOnlyMatchingType()
{
// Arrange
var ledger = new Ledger();
var expenseAllocations = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(100m, "USD") } };
var incomeAllocations = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(1000m, "USD") } };
var expense = new LedgerEntry(DateTime.Now, "Expense", new Money(100m, "USD"), EntryType.Expense, expenseAllocations);
var income = new LedgerEntry(DateTime.Now, "Income", new Money(1000m, "USD"), EntryType.Income, incomeAllocations);
ledger.AddEntry(expense);
ledger.AddEntry(income);
// Act
var expenses = ledger.GetEntriesByType(EntryType.Expense);
var incomes = ledger.GetEntriesByType(EntryType.Income);
// Assert
Assert.Single(expenses);
Assert.Single(incomes);
Assert.Equal(EntryType.Expense, expenses.First().Type);
Assert.Equal(EntryType.Income, incomes.First().Type);
}
[Fact]
public void CalculateTotalByType_ReturnsCorrectTotal()
{
// Arrange
var ledger = new Ledger();
var expenseAllocations1 = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(100m, "USD") } };
var expenseAllocations2 = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(200m, "USD") } };
var expense1 = new LedgerEntry(DateTime.Now, "Expense 1", new Money(100m, "USD"), EntryType.Expense, expenseAllocations1);
var expense2 = new LedgerEntry(DateTime.Now, "Expense 2", new Money(200m, "USD"), EntryType.Expense, expenseAllocations2);
ledger.AddEntry(expense1);
ledger.AddEntry(expense2);
// Act
var total = ledger.CalculateTotalByType(EntryType.Expense, "USD");
// Assert
Assert.Equal(300m, total.Amount);
}
[Fact]
public void CalculateNetBalance_ReturnsIncomeMinusExpenses()
{
// Arrange
var ledger = new Ledger();
var expenseAllocations = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(100m, "USD") } };
var incomeAllocations = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(1000m, "USD") } };
var expense = new LedgerEntry(DateTime.Now, "Expense", new Money(100m, "USD"), EntryType.Expense, expenseAllocations);
var income = new LedgerEntry(DateTime.Now, "Income", new Money(1000m, "USD"), EntryType.Income, incomeAllocations);
ledger.AddEntry(expense);
ledger.AddEntry(income);
// Act
var netBalance = ledger.CalculateNetBalance("USD");
// Assert
Assert.Equal(900m, netBalance.Amount);
}
[Fact]
public void GetEntriesForBudget_ReturnsEntriesWithBudgetAllocation()
{
// Arrange
var budgetId = Guid.NewGuid();
var ledger = new Ledger();
var allocations1 = new Dictionary<Guid, Money> { { budgetId, new Money(100m, "USD") } };
var allocations2 = new Dictionary<Guid, Money> { { Guid.NewGuid(), new Money(200m, "USD") } };
var entry1 = new LedgerEntry(DateTime.Now, "Entry 1", new Money(100m, "USD"), EntryType.Expense, allocations1);
var entry2 = new LedgerEntry(DateTime.Now, "Entry 2", new Money(200m, "USD"), EntryType.Expense, allocations2);
ledger.AddEntry(entry1);
ledger.AddEntry(entry2);
// Act
var entries = ledger.GetEntriesForBudget(budgetId);
// Assert
Assert.Single(entries);
Assert.Equal(entry1, entries.First());
}
}

View File

@ -0,0 +1,124 @@
namespace BudgetApp.Tests.Domain;
using BudgetApp.Domain.Models;
using Xunit;
public class MoneyTests
{
[Fact]
public void Constructor_WithValidAmount_CreatesMoney()
{
// Arrange & Act
var money = new Money(100.50m, "USD");
// Assert
Assert.Equal(100.50m, money.Amount);
Assert.Equal("USD", money.Currency);
}
[Fact]
public void Constructor_WithNegativeAmount_ThrowsException()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentException>(() => new Money(-100m, "USD"));
}
[Fact]
public void Constructor_WithNullCurrency_ThrowsException()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentException>(() => new Money(100m, null!));
Assert.Throws<ArgumentException>(() => new Money(100m, ""));
}
[Fact]
public void Add_WithSameCurrency_ReturnsSum()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
// Act
var result = money1.Add(money2);
// Assert
Assert.Equal(150m, result.Amount);
Assert.Equal("USD", result.Currency);
}
[Fact]
public void Add_WithDifferentCurrencies_ThrowsException()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "EUR");
// Act & Assert
Assert.Throws<InvalidOperationException>(() => money1.Add(money2));
}
[Fact]
public void Subtract_WithSameCurrency_ReturnsDifference()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
// Act
var result = money1.Subtract(money2);
// Assert
Assert.Equal(50m, result.Amount);
}
[Fact]
public void Subtract_ResultingInNegative_ThrowsException()
{
// Arrange
var money1 = new Money(50m, "USD");
var money2 = new Money(100m, "USD");
// Act & Assert
Assert.Throws<InvalidOperationException>(() => money1.Subtract(money2));
}
[Fact]
public void Equals_WithSameAmountAndCurrency_ReturnsTrue()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(100m, "USD");
// Act & Assert
Assert.True(money1.Equals(money2));
Assert.True(money1 == money2);
}
[Fact]
public void Equals_WithDifferentAmount_ReturnsFalse()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
// Act & Assert
Assert.False(money1.Equals(money2));
Assert.True(money1 != money2);
}
[Fact]
public void OperatorAdd_WithSameCurrency_ReturnsSum()
{
// Arrange
var money1 = new Money(100m, "USD");
var money2 = new Money(50m, "USD");
// Act
var result = money1 + money2;
// Assert
Assert.Equal(150m, result.Amount);
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More