Files
2026-03-06 11:05:01 +00:00

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
}