261 lines
12 KiB
C#
261 lines
12 KiB
C#
using BudgetApp.Data.Entities;
|
|
using BudgetApp.Data.Repositories;
|
|
using BudgetApp.Services;
|
|
using BudgetApp.Tests.Helpers;
|
|
using Xunit;
|
|
|
|
namespace BudgetApp.Tests.Services;
|
|
|
|
public class DashboardServiceTests
|
|
{
|
|
#region BuildChartData (internal static — testable directly)
|
|
|
|
[Fact]
|
|
public void BuildChartData_GeneratesLabelsForDateRange()
|
|
{
|
|
var start = new DateTime(2025, 1, 1);
|
|
var end = new DateTime(2025, 1, 5);
|
|
var accounts = new List<Account>();
|
|
var transactions = new List<Transaction>();
|
|
|
|
var result = DashboardService.BuildChartData(accounts, transactions, start, end);
|
|
|
|
Assert.Equal(5, result.Labels.Count);
|
|
Assert.Equal("Jan 01", result.Labels[0]);
|
|
Assert.Equal("Jan 05", result.Labels[4]);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_GeneratesDayTransactionLabelsForDateRange()
|
|
{
|
|
var start = new DateTime(2025, 1, 1);
|
|
var end = new DateTime(2025, 1, 3);
|
|
var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP" };
|
|
var transactions = new List<Transaction>
|
|
{
|
|
new() { Description = "Salary", Amount = 1000, Date = new DateTime(2025, 1, 1), Type = TransactionType.Income, AccountId = 1, Account = account },
|
|
new() { Description = "Lunch", Amount = 10, Date = new DateTime(2025, 1, 2), Type = TransactionType.Expense, AccountId = 1, Account = account }
|
|
};
|
|
|
|
var result = DashboardService.BuildChartData([account], transactions, start, end);
|
|
|
|
Assert.Equal(3, result.DayTransactionLabels.Count);
|
|
Assert.Single(result.DayTransactionLabels[0]); // Jan 1 — Salary
|
|
Assert.Contains("+£1,000.00 Salary (Current)", result.DayTransactionLabels[0][0]);
|
|
Assert.Single(result.DayTransactionLabels[1]); // Jan 2 — Lunch
|
|
Assert.Contains("-£10.00 Lunch (Current)", result.DayTransactionLabels[1][0]);
|
|
Assert.Empty(result.DayTransactionLabels[2]); // Jan 3 — empty
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_CalculatesRunningBalance()
|
|
{
|
|
var start = new DateTime(2025, 1, 1);
|
|
var end = new DateTime(2025, 1, 3);
|
|
var today = DateTime.Today;
|
|
|
|
// Account has current balance of 500
|
|
var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP", CurrentBalance = 500 };
|
|
|
|
// All transactions are in the past or at start
|
|
var transactions = new List<Transaction>
|
|
{
|
|
new() { Description = "Income", Amount = 200, Date = new DateTime(2025, 1, 1), Type = TransactionType.Income, AccountId = 1, Account = account },
|
|
new() { Description = "Expense", Amount = 50, Date = new DateTime(2025, 1, 2), Type = TransactionType.Expense, AccountId = 1, Account = account }
|
|
};
|
|
|
|
var result = DashboardService.BuildChartData([account], transactions, start, end);
|
|
|
|
Assert.Single(result.Datasets);
|
|
var data = result.Datasets[0].Data;
|
|
Assert.Equal(3, data.Count);
|
|
// The chart works backward from CurrentBalance to find starting point, then re-applies
|
|
// Since these transactions are far in the past relative to "today", the behavior depends on
|
|
// the start date vs today comparison. We validate the dataset structure is correct.
|
|
Assert.Equal("Current", result.Datasets[0].Label);
|
|
Assert.Equal("#000", result.Datasets[0].Color);
|
|
Assert.Equal("£", result.Datasets[0].CurrencySymbol);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_MultipleAccounts_CreatesDatasetPerAccount()
|
|
{
|
|
var start = new DateTime(2025, 1, 1);
|
|
var end = new DateTime(2025, 1, 2);
|
|
|
|
var account1 = new Account { Id = 1, Name = "Current", Color = "#111", CurrencyCode = "GBP", CurrentBalance = 100 };
|
|
var account2 = new Account { Id = 2, Name = "Savings", Color = "#222", CurrencyCode = "USD", CurrentBalance = 200 };
|
|
|
|
var result = DashboardService.BuildChartData([account1, account2], [], start, end);
|
|
|
|
Assert.Equal(2, result.Datasets.Count);
|
|
Assert.Equal("Current", result.Datasets[0].Label);
|
|
Assert.Equal("Savings", result.Datasets[1].Label);
|
|
Assert.Equal("£", result.Datasets[0].CurrencySymbol);
|
|
Assert.Equal("$", result.Datasets[1].CurrencySymbol);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_EmptyAccounts_ReturnsEmptyDatasets()
|
|
{
|
|
var start = new DateTime(2025, 1, 1);
|
|
var end = new DateTime(2025, 1, 5);
|
|
|
|
var result = DashboardService.BuildChartData([], [], start, end);
|
|
|
|
Assert.Equal(5, result.Labels.Count);
|
|
Assert.Empty(result.Datasets);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_PaidFutureTransaction_ExcludedFromProjection()
|
|
{
|
|
var today = DateTime.Today;
|
|
var start = today.AddDays(-1);
|
|
var end = today.AddDays(5);
|
|
|
|
// CurrentBalance is manually maintained by the user — MarkAsPaidAsync never touches it
|
|
var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP", CurrentBalance = 1000 };
|
|
var transactions = new List<Transaction>
|
|
{
|
|
// Unpaid future expense — should reduce the projected balance
|
|
new() { Description = "Unpaid Bill", Amount = 100, Date = today.AddDays(3), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = false },
|
|
// Paid future expense — excluded from projection; CurrentBalance already reflects it (user-managed)
|
|
new() { Description = "Paid Bill", Amount = 200, Date = today.AddDays(4), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = true }
|
|
};
|
|
|
|
var result = DashboardService.BuildChartData([account], transactions, start, end);
|
|
|
|
var data = result.Datasets[0].Data;
|
|
// Today's index = 1 (start is today-1)
|
|
int todayIndex = 1;
|
|
// Balance is flat at CurrentBalance up to the unpaid bill
|
|
Assert.Equal(1000m, data[todayIndex]);
|
|
// Day +3 (index 4): unpaid bill applied → 900
|
|
Assert.Equal(900m, data[todayIndex + 3]);
|
|
// Day +4 (index 5): paid bill is excluded from projection → stays at 900
|
|
Assert.Equal(900m, data[todayIndex + 4]);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildChartData_PaidFutureTransaction_ExcludedFromDayTransactionLabels()
|
|
{
|
|
var today = DateTime.Today;
|
|
var start = today;
|
|
var end = today.AddDays(2);
|
|
|
|
var account = new Account { Id = 1, Name = "Current", Color = "#000", CurrencyCode = "GBP", CurrentBalance = 500 };
|
|
var transactions = new List<Transaction>
|
|
{
|
|
new() { Description = "Unpaid", Amount = 50, Date = today.AddDays(1), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = false },
|
|
new() { Description = "Paid", Amount = 100, Date = today.AddDays(2), Type = TransactionType.Expense, AccountId = 1, Account = account, IsPaid = true }
|
|
};
|
|
|
|
var result = DashboardService.BuildChartData([account], transactions, start, end);
|
|
|
|
// Day +1: unpaid transaction appears in labels
|
|
Assert.Single(result.DayTransactionLabels[1]);
|
|
Assert.Contains("Unpaid", result.DayTransactionLabels[1][0]);
|
|
// Day +2: paid transaction is excluded from labels
|
|
Assert.Empty(result.DayTransactionLabels[2]);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetDashboardDataAsync (integration tests)
|
|
|
|
[Fact]
|
|
public async Task GetDashboardDataAsync_ReturnsPopulatedData()
|
|
{
|
|
using var factory = new TestDbContextFactory();
|
|
using (var ctx = factory.CreateContext())
|
|
{
|
|
var account = new Account { Name = "Current", UserId = TestDbContextFactory.TestUserId, CurrencyCode = "GBP" };
|
|
ctx.Accounts.Add(account);
|
|
ctx.SaveChanges();
|
|
|
|
ctx.Transactions.AddRange(
|
|
new Transaction { Description = "Past", Amount = 10, Date = DateTime.Today.AddDays(-5), Type = TransactionType.Expense, AccountId = account.Id },
|
|
new Transaction { Description = "Future", Amount = 20, Date = DateTime.Today.AddDays(5), Type = TransactionType.Income, AccountId = account.Id });
|
|
ctx.SaveChanges();
|
|
}
|
|
|
|
using var context = factory.CreateContext();
|
|
var transactionService = new TransactionRepository(context);
|
|
var accountService = new AccountRepository(context);
|
|
var categoryService = new CategoryRepository(context);
|
|
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
|
|
|
|
var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId);
|
|
|
|
Assert.NotEmpty(result.Accounts);
|
|
Assert.NotEmpty(result.Categories); // defaults seeded
|
|
Assert.NotEmpty(result.RecentTransactions);
|
|
Assert.NotEmpty(result.UpcomingTransactions);
|
|
Assert.NotNull(result.ChartData);
|
|
Assert.Equal("£", result.CurrencySymbol);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDashboardDataAsync_SplitsRecentAndUpcoming()
|
|
{
|
|
using var factory = new TestDbContextFactory();
|
|
using (var ctx = factory.CreateContext())
|
|
{
|
|
var account = new Account { Name = "Test", UserId = TestDbContextFactory.TestUserId };
|
|
ctx.Accounts.Add(account);
|
|
ctx.SaveChanges();
|
|
|
|
ctx.Transactions.AddRange(
|
|
new Transaction { Description = "Yesterday", Amount = 10, Date = DateTime.Today.AddDays(-1), AccountId = account.Id },
|
|
new Transaction { Description = "Today", Amount = 20, Date = DateTime.Today, AccountId = account.Id },
|
|
new Transaction { Description = "Tomorrow", Amount = 30, Date = DateTime.Today.AddDays(1), AccountId = account.Id });
|
|
ctx.SaveChanges();
|
|
}
|
|
|
|
using var context = factory.CreateContext();
|
|
var transactionService = new TransactionRepository(context);
|
|
var accountService = new AccountRepository(context);
|
|
var categoryService = new CategoryRepository(context);
|
|
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
|
|
|
|
var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId);
|
|
|
|
// Recent = today and before, Upcoming = after today
|
|
Assert.All(result.RecentTransactions, t => Assert.True(t.Date.Date <= DateTime.Today));
|
|
Assert.All(result.UpcomingTransactions, t => Assert.True(t.Date.Date > DateTime.Today));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDashboardDataAsync_WithCustomDateRange_FiltersCorrectly()
|
|
{
|
|
using var factory = new TestDbContextFactory();
|
|
using (var ctx = factory.CreateContext())
|
|
{
|
|
var account = new Account { Name = "Test", UserId = TestDbContextFactory.TestUserId };
|
|
ctx.Accounts.Add(account);
|
|
ctx.SaveChanges();
|
|
|
|
ctx.Transactions.AddRange(
|
|
new Transaction { Description = "InRange", Amount = 10, Date = DateTime.Today.AddDays(-5), AccountId = account.Id },
|
|
new Transaction { Description = "OutOfRange", Amount = 20, Date = DateTime.Today.AddDays(-60), AccountId = account.Id });
|
|
ctx.SaveChanges();
|
|
}
|
|
|
|
using var context = factory.CreateContext();
|
|
var transactionService = new TransactionRepository(context);
|
|
var accountService = new AccountRepository(context);
|
|
var categoryService = new CategoryRepository(context);
|
|
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
|
|
|
|
var from = DateTime.Today.AddDays(-10);
|
|
var to = DateTime.Today.AddDays(10);
|
|
var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId, from, to);
|
|
|
|
Assert.Contains(result.RecentTransactions, t => t.Description == "InRange");
|
|
Assert.DoesNotContain(result.RecentTransactions, t => t.Description == "OutOfRange");
|
|
}
|
|
|
|
#endregion
|
|
}
|