Initial commit
This commit is contained in:
parent
3f854cefc6
commit
dcf2ad67bb
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,4 +11,3 @@
|
|||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
|||||||
153
BudgetApp.Tests/Application/BudgetServiceTests.cs
Normal file
153
BudgetApp.Tests/Application/BudgetServiceTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
255
BudgetApp.Tests/Application/LedgerServiceTests.cs
Normal file
255
BudgetApp.Tests/Application/LedgerServiceTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
30
BudgetApp.Tests/BudgetApp.Tests.csproj
Normal file
30
BudgetApp.Tests/BudgetApp.Tests.csproj
Normal 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>
|
||||||
|
|
||||||
90
BudgetApp.Tests/Domain/BaseBudgetTests.cs
Normal file
90
BudgetApp.Tests/Domain/BaseBudgetTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
158
BudgetApp.Tests/Domain/GoalBudgetTests.cs
Normal file
158
BudgetApp.Tests/Domain/GoalBudgetTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
148
BudgetApp.Tests/Domain/LedgerEntryTests.cs
Normal file
148
BudgetApp.Tests/Domain/LedgerEntryTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
152
BudgetApp.Tests/Domain/LedgerTests.cs
Normal file
152
BudgetApp.Tests/Domain/LedgerTests.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
124
BudgetApp.Tests/Domain/MoneyTests.cs
Normal file
124
BudgetApp.Tests/Domain/MoneyTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1732
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.deps.json
Normal file
1732
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.dll
Normal file
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.dll
Normal file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.pdb
Normal file
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.Tests.pdb
Normal file
Binary file not shown.
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"framework": {
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
"configProperties": {
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.dll
Normal file
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.dll
Normal file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.pdb
Normal file
BIN
BudgetApp.Tests/bin/Debug/net8.0/BudgetApp.pdb
Normal file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Castle.Core.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Castle.Core.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.CoreUtilities.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.CoreUtilities.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.CrossPlatEngine.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.CrossPlatEngine.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.PlatformAbstractions.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.PlatformAbstractions.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.Utilities.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.TestPlatform.Utilities.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.VisualStudio.TestPlatform.Common.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Microsoft.VisualStudio.TestPlatform.Common.dll
Executable file
Binary file not shown.
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Moq.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Moq.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/Newtonsoft.Json.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/Newtonsoft.Json.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/NuGet.Frameworks.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/NuGet.Frameworks.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/System.Diagnostics.EventLog.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/System.Diagnostics.EventLog.dll
Executable file
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.
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.
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.
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.
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.
BIN
BudgetApp.Tests/bin/Debug/net8.0/testhost.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/testhost.dll
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.abstractions.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.abstractions.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.assert.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.assert.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.core.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.core.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.execution.dotnet.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.execution.dotnet.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.runner.reporters.netcoreapp10.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.runner.reporters.netcoreapp10.dll
Executable file
Binary file not shown.
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.runner.utility.netcoreapp10.dll
Executable file
BIN
BudgetApp.Tests/bin/Debug/net8.0/xunit.runner.utility.netcoreapp10.dll
Executable file
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
Loading…
x
Reference in New Issue
Block a user