Add tests
This commit is contained in:
1 parent
d5127c7405
commit
88aaaecb1a
15 files changed
+1755
-34
No files matched your search
@@ -0,0 +1,876 @@
|
||||
using BudgetApp.Data;
|
||||
using BudgetApp.Services;
|
||||
using BudgetApp.Tests.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace BudgetApp.Tests.Services;
|
||||
|
||||
public class TransactionServiceTests
|
||||
{
|
||||
private static Account SeedAccount(ApplicationDbContext ctx, string userId = TestDbContextFactory.TestUserId)
|
||||
{
|
||||
var account = new Account { Name = "Test Account", UserId = userId };
|
||||
ctx.Accounts.Add(account);
|
||||
ctx.SaveChanges();
|
||||
return account;
|
||||
}
|
||||
|
||||
#region GetTransactionsAsync
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsAsync_ReturnsUserTransactions_OrderedByDateDescending()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Old", Amount = 10, Date = new DateTime(2025, 1, 1), AccountId = account.Id },
|
||||
new Transaction { Description = "New", Amount = 20, Date = new DateTime(2025, 6, 1), AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("New", result[0].Description);
|
||||
Assert.Equal("Old", result[1].Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsAsync_ExcludesOtherUserTransactions()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var mine = SeedAccount(ctx, TestDbContextFactory.TestUserId);
|
||||
var theirs = SeedAccount(ctx, TestDbContextFactory.OtherUserId);
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Mine", Amount = 10, Date = DateTime.Today, AccountId = mine.Id },
|
||||
new Transaction { Description = "Theirs", Amount = 20, Date = DateTime.Today, AccountId = theirs.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Mine", result[0].Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsAsync_IncludesRelatedEntities()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var category = new Category { Name = "Food", UserId = TestDbContextFactory.TestUserId };
|
||||
ctx.Categories.Add(category);
|
||||
ctx.SaveChanges();
|
||||
|
||||
ctx.Transactions.Add(new Transaction
|
||||
{
|
||||
Description = "Lunch",
|
||||
Amount = 10,
|
||||
Date = DateTime.Today,
|
||||
AccountId = account.Id,
|
||||
CategoryId = category.Id
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.NotNull(result[0].Account);
|
||||
Assert.NotNull(result[0].Category);
|
||||
Assert.Equal("Food", result[0].Category!.Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetRecentTransactionsAsync
|
||||
|
||||
[Fact]
|
||||
public async Task GetRecentTransactionsAsync_ReturnsTransactionsOnOrBeforeToday()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Past", 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 = "Future", Amount = 30, Date = DateTime.Today.AddDays(1), AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 10);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.DoesNotContain(result, t => t.Description == "Future");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRecentTransactionsAsync_RespectsCount()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ctx.Transactions.Add(new Transaction
|
||||
{
|
||||
Description = $"T{i}",
|
||||
Amount = i,
|
||||
Date = DateTime.Today.AddDays(-i),
|
||||
AccountId = account.Id
|
||||
});
|
||||
}
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 3);
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUpcomingTransactionsAsync
|
||||
|
||||
[Fact]
|
||||
public async Task GetUpcomingTransactionsAsync_ReturnsTransactionsAfterToday()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Past", 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 = "Future", Amount = 30, Date = DateTime.Today.AddDays(1), AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetUpcomingTransactionsAsync(TestDbContextFactory.TestUserId, 10);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Future", result[0].Description);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetTransactionsInRangeAsync
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsInRangeAsync_ReturnsTransactionsInRange()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
var from = new DateTime(2025, 3, 1);
|
||||
var to = new DateTime(2025, 3, 31);
|
||||
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Before", Amount = 10, Date = new DateTime(2025, 2, 28), AccountId = account.Id },
|
||||
new Transaction { Description = "Start", Amount = 20, Date = from, AccountId = account.Id },
|
||||
new Transaction { Description = "Middle", Amount = 30, Date = new DateTime(2025, 3, 15), AccountId = account.Id },
|
||||
new Transaction { Description = "End", Amount = 40, Date = to, AccountId = account.Id },
|
||||
new Transaction { Description = "After", Amount = 50, Date = new DateTime(2025, 4, 1), AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsInRangeAsync(TestDbContextFactory.TestUserId, from, to);
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.DoesNotContain(result, t => t.Description == "Before");
|
||||
Assert.DoesNotContain(result, t => t.Description == "After");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetTransactionsByAccountAsync
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsByAccountAsync_ReturnsAccountTransactions()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account1 = SeedAccount(ctx);
|
||||
var account2 = new Account { Name = "Other", UserId = TestDbContextFactory.TestUserId };
|
||||
ctx.Accounts.Add(account2);
|
||||
ctx.SaveChanges();
|
||||
accountId = account1.Id;
|
||||
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "A1", Amount = 10, Date = DateTime.Today, AccountId = account1.Id },
|
||||
new Transaction { Description = "A2", Amount = 20, Date = DateTime.Today, AccountId = account2.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.TestUserId);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("A1", result[0].Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTransactionsByAccountAsync_WrongUser_ReturnsEmpty()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx, TestDbContextFactory.TestUserId);
|
||||
accountId = account.Id;
|
||||
ctx.Transactions.Add(new Transaction { Description = "T", Amount = 10, Date = DateTime.Today, AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.OtherUserId);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateTransactionAsync
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_AddsTransaction()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "Groceries",
|
||||
Amount = 50,
|
||||
Date = DateTime.Today,
|
||||
Type = TransactionType.Expense,
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Single(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_NonRecurring_DoesNotCreateChildren()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "One-off",
|
||||
Amount = 10,
|
||||
Date = DateTime.Today,
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Single(verifyCtx.Transactions);
|
||||
Assert.Null(verifyCtx.Transactions.Single().ParentTransactionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_Recurring_CapsEndDateToOneYear()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
var startDate = new DateTime(2025, 1, 1);
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = startDate.AddYears(5), // way beyond 1 year
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var parent = await verifyCtx.Transactions
|
||||
.FirstAsync(t => t.ParentTransactionId == null);
|
||||
|
||||
Assert.Equal(startDate.AddYears(1), parent.RecurrenceEndDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_Recurring_DefaultsEndDateWhenNull()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
var startDate = new DateTime(2025, 1, 1);
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = null,
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var parent = await verifyCtx.Transactions
|
||||
.FirstAsync(t => t.ParentTransactionId == null);
|
||||
|
||||
Assert.Equal(startDate.AddYears(1), parent.RecurrenceEndDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_Recurring_MaterializesChildren()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
var startDate = new DateTime(2025, 1, 1);
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "Weekly",
|
||||
Amount = 50,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Weekly,
|
||||
RecurrenceEndDate = startDate.AddDays(21), // 3 weeks = 3 children
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var all = await verifyCtx.Transactions.ToListAsync();
|
||||
var parent = all.Single(t => t.ParentTransactionId == null);
|
||||
var children = all.Where(t => t.ParentTransactionId == parent.Id).ToList();
|
||||
|
||||
Assert.Equal(3, children.Count);
|
||||
Assert.All(children, c =>
|
||||
{
|
||||
Assert.Equal("Weekly", c.Description);
|
||||
Assert.Equal(50m, c.Amount);
|
||||
Assert.Equal(RecurrenceType.None, c.RecurrenceType);
|
||||
Assert.Equal(parent.Id, c.ParentTransactionId);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTransactionAsync_Recurring_Monthly_MaterializesCorrectDates()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int accountId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
accountId = SeedAccount(ctx).Id;
|
||||
}
|
||||
|
||||
var startDate = new DateTime(2025, 1, 15);
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.CreateTransactionAsync(new Transaction
|
||||
{
|
||||
Description = "Monthly",
|
||||
Amount = 100,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = new DateTime(2025, 4, 15), // 3 months → 3 children
|
||||
AccountId = accountId
|
||||
});
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var children = await verifyCtx.Transactions
|
||||
.Where(t => t.ParentTransactionId != null)
|
||||
.OrderBy(t => t.Date)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(3, children.Count);
|
||||
Assert.Equal(new DateTime(2025, 2, 15), children[0].Date);
|
||||
Assert.Equal(new DateTime(2025, 3, 15), children[1].Date);
|
||||
Assert.Equal(new DateTime(2025, 4, 15), children[2].Date);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateTransactionAsync
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTransactionAsync_UpdatesAllFields()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int transactionId, accountId, newAccountId;
|
||||
int categoryId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
accountId = account.Id;
|
||||
var newAccount = new Account { Name = "Other", UserId = TestDbContextFactory.TestUserId };
|
||||
ctx.Accounts.Add(newAccount);
|
||||
var category = new Category { Name = "Food", UserId = TestDbContextFactory.TestUserId };
|
||||
ctx.Categories.Add(category);
|
||||
ctx.SaveChanges();
|
||||
newAccountId = newAccount.Id;
|
||||
categoryId = category.Id;
|
||||
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Description = "Old",
|
||||
Amount = 10,
|
||||
Date = DateTime.Today,
|
||||
Type = TransactionType.Expense,
|
||||
AccountId = accountId
|
||||
};
|
||||
ctx.Transactions.Add(transaction);
|
||||
ctx.SaveChanges();
|
||||
transactionId = transaction.Id;
|
||||
}
|
||||
|
||||
var newDate = DateTime.Today.AddDays(5);
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.UpdateTransactionAsync(
|
||||
transactionId, TestDbContextFactory.TestUserId,
|
||||
"Updated", 99m, newDate, TransactionType.Income,
|
||||
newAccountId, categoryId, RecurrenceType.Weekly, newDate.AddMonths(3));
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var updated = verifyCtx.Transactions.Single();
|
||||
Assert.Equal("Updated", updated.Description);
|
||||
Assert.Equal(99m, updated.Amount);
|
||||
Assert.Equal(newDate, updated.Date);
|
||||
Assert.Equal(TransactionType.Income, updated.Type);
|
||||
Assert.Equal(newAccountId, updated.AccountId);
|
||||
Assert.Equal(categoryId, updated.CategoryId);
|
||||
Assert.Equal(RecurrenceType.Weekly, updated.RecurrenceType);
|
||||
Assert.Equal(newDate.AddMonths(3), updated.RecurrenceEndDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTransactionAsync_WrongUser_DoesNothing()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int transactionId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Description = "Original",
|
||||
Amount = 10,
|
||||
Date = DateTime.Today,
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(transaction);
|
||||
ctx.SaveChanges();
|
||||
transactionId = transaction.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.UpdateTransactionAsync(
|
||||
transactionId, TestDbContextFactory.OtherUserId,
|
||||
"Hacked", 999m, DateTime.Today, TransactionType.Income, 1, null);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Equal("Original", verifyCtx.Transactions.Single().Description);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteTransactionAsync
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionAsync_RemovesTransaction()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int transactionId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var transaction = new Transaction { Description = "ToDelete", Amount = 10, Date = DateTime.Today, AccountId = account.Id };
|
||||
ctx.Transactions.Add(transaction);
|
||||
ctx.SaveChanges();
|
||||
transactionId = transaction.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.TestUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Empty(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionAsync_WrongUser_DoesNothing()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int transactionId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var transaction = new Transaction { Description = "Protected", Amount = 10, Date = DateTime.Today, AccountId = account.Id };
|
||||
ctx.Transactions.Add(transaction);
|
||||
ctx.SaveChanges();
|
||||
transactionId = transaction.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.OtherUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Single(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteTransactionSeriesAsync
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionSeriesAsync_ChildClick_DeletesFromCutoffForward()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int childId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var parent = new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = new DateTime(2025, 1, 1),
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = new DateTime(2025, 6, 1),
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(parent);
|
||||
ctx.SaveChanges();
|
||||
|
||||
// Add children: Feb, Mar, Apr, May, Jun
|
||||
var children = new List<Transaction>();
|
||||
for (int m = 2; m <= 6; m++)
|
||||
{
|
||||
children.Add(new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = new DateTime(2025, m, 1),
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
ParentTransactionId = parent.Id,
|
||||
AccountId = account.Id
|
||||
});
|
||||
}
|
||||
ctx.Transactions.AddRange(children);
|
||||
ctx.SaveChanges();
|
||||
|
||||
// Click on the April child (cutoff = Apr 1)
|
||||
childId = children[2].Id; // Apr
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var remaining = await verifyCtx.Transactions.OrderBy(t => t.Date).ToListAsync();
|
||||
|
||||
// Parent (Jan) + Feb + Mar should remain; Apr, May, Jun deleted
|
||||
Assert.Equal(3, remaining.Count);
|
||||
Assert.Contains(remaining, t => t.Date.Month == 1 && t.ParentTransactionId == null); // parent
|
||||
Assert.Contains(remaining, t => t.Date.Month == 2);
|
||||
Assert.Contains(remaining, t => t.Date.Month == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionSeriesAsync_ChildClick_CapsParentRecurrence()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int childId, parentId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var parent = new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = new DateTime(2025, 1, 1),
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = new DateTime(2025, 12, 1),
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(parent);
|
||||
ctx.SaveChanges();
|
||||
parentId = parent.Id;
|
||||
|
||||
var child = new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = new DateTime(2025, 4, 1),
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
ParentTransactionId = parent.Id,
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(child);
|
||||
ctx.SaveChanges();
|
||||
childId = child.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var updatedParent = await verifyCtx.Transactions.FindAsync(parentId);
|
||||
Assert.NotNull(updatedParent);
|
||||
Assert.Equal(new DateTime(2025, 3, 31), updatedParent.RecurrenceEndDate); // cutoff - 1 day
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionSeriesAsync_ParentClick_DeletesParentAndAllChildren()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int parentId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var parent = new Transaction
|
||||
{
|
||||
Description = "Recurring",
|
||||
Amount = 100,
|
||||
Date = new DateTime(2025, 1, 1),
|
||||
RecurrenceType = RecurrenceType.Monthly,
|
||||
RecurrenceEndDate = new DateTime(2025, 3, 1),
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(parent);
|
||||
ctx.SaveChanges();
|
||||
parentId = parent.Id;
|
||||
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 2, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id },
|
||||
new Transaction { Description = "Recurring", Amount = 100, Date = new DateTime(2025, 3, 1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionSeriesAsync(parentId, TestDbContextFactory.TestUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Empty(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTransactionSeriesAsync_WrongUser_DoesNothing()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int transactionId;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var transaction = new Transaction { Description = "Protected", Amount = 10, Date = DateTime.Today, AccountId = account.Id };
|
||||
ctx.Transactions.Add(transaction);
|
||||
ctx.SaveChanges();
|
||||
transactionId = transaction.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.DeleteTransactionSeriesAsync(transactionId, TestDbContextFactory.OtherUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Single(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MaterializeRecurringTransactionsAsync
|
||||
|
||||
[Fact]
|
||||
public async Task MaterializeRecurringTransactionsAsync_FillsGaps()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int parentId;
|
||||
var startDate = DateTime.Today;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var parent = new Transaction
|
||||
{
|
||||
Description = "Daily",
|
||||
Amount = 5,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Daily,
|
||||
RecurrenceEndDate = startDate.AddDays(10),
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(parent);
|
||||
ctx.SaveChanges();
|
||||
parentId = parent.Id;
|
||||
|
||||
// Only 2 children exist (days 1 and 2)
|
||||
ctx.Transactions.AddRange(
|
||||
new Transaction { Description = "Daily", Amount = 5, Date = startDate.AddDays(1), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id },
|
||||
new Transaction { Description = "Daily", Amount = 5, Date = startDate.AddDays(2), RecurrenceType = RecurrenceType.None, ParentTransactionId = parent.Id, AccountId = account.Id });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(100));
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var children = await verifyCtx.Transactions
|
||||
.Where(t => t.ParentTransactionId == parentId)
|
||||
.ToListAsync();
|
||||
|
||||
// Days 1-10 should be filled = 10 children
|
||||
Assert.Equal(10, children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MaterializeRecurringTransactionsAsync_RespectsRecurrenceEndDate()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
int parentId;
|
||||
var startDate = DateTime.Today;
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
var parent = new Transaction
|
||||
{
|
||||
Description = "Weekly",
|
||||
Amount = 10,
|
||||
Date = startDate,
|
||||
RecurrenceType = RecurrenceType.Weekly,
|
||||
RecurrenceEndDate = startDate.AddDays(14), // 2 weeks
|
||||
AccountId = account.Id
|
||||
};
|
||||
ctx.Transactions.Add(parent);
|
||||
ctx.SaveChanges();
|
||||
parentId = parent.Id;
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(365));
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
var children = await verifyCtx.Transactions
|
||||
.Where(t => t.ParentTransactionId == parentId)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Equal(2, children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MaterializeRecurringTransactionsAsync_IgnoresNonParentRecurring()
|
||||
{
|
||||
using var factory = new TestDbContextFactory();
|
||||
using (var ctx = factory.CreateContext())
|
||||
{
|
||||
var account = SeedAccount(ctx);
|
||||
// A non-recurring transaction
|
||||
ctx.Transactions.Add(new Transaction
|
||||
{
|
||||
Description = "Single",
|
||||
Amount = 10,
|
||||
Date = DateTime.Today,
|
||||
RecurrenceType = RecurrenceType.None,
|
||||
AccountId = account.Id
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
using var context = factory.CreateContext();
|
||||
var service = new TransactionService(context);
|
||||
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId);
|
||||
|
||||
using var verifyCtx = factory.CreateContext();
|
||||
Assert.Single(verifyCtx.Transactions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetNextOccurrence
|
||||
|
||||
[Theory]
|
||||
[InlineData(RecurrenceType.Daily, 1, 0, 0)]
|
||||
[InlineData(RecurrenceType.Weekly, 7, 0, 0)]
|
||||
[InlineData(RecurrenceType.Monthly, 0, 1, 0)]
|
||||
[InlineData(RecurrenceType.Yearly, 0, 0, 1)]
|
||||
public void GetNextOccurrence_ReturnsCorrectDate(RecurrenceType type, int expectedDays, int expectedMonths, int expectedYears)
|
||||
{
|
||||
var current = new DateTime(2025, 6, 15);
|
||||
var expected = current.AddDays(expectedDays).AddMonths(expectedMonths).AddYears(expectedYears);
|
||||
|
||||
var result = TransactionService.GetNextOccurrence(current, type);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNextOccurrence_None_ReturnsMaxValue()
|
||||
{
|
||||
var result = TransactionService.GetNextOccurrence(DateTime.Today, RecurrenceType.None);
|
||||
Assert.Equal(DateTime.MaxValue, result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in new issue
Block a user