using BudgetApp.Data; using BudgetApp.Services; using BudgetApp.Tests.Helpers; using Microsoft.EntityFrameworkCore; using Xunit; namespace BudgetApp.Tests.Services; public class CategoryServiceTests { [Fact] public async Task GetCategoriesAsync_ReturnsOnlyUserCategories() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { ctx.Categories.AddRange( new Category { Name = "Mine", UserId = TestDbContextFactory.TestUserId }, new Category { Name = "Theirs", UserId = TestDbContextFactory.OtherUserId }); await ctx.SaveChangesAsync(); } using var context = factory.CreateContext(); var service = new CategoryService(context); var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId); Assert.Single(result); Assert.Equal("Mine", result[0].Name); } [Fact] public async Task GetCategoriesAsync_OrderedByName() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { ctx.Categories.AddRange( new Category { Name = "Zebra", UserId = TestDbContextFactory.TestUserId }, new Category { Name = "Alpha", UserId = TestDbContextFactory.TestUserId }); await ctx.SaveChangesAsync(); } using var context = factory.CreateContext(); var service = new CategoryService(context); var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId); Assert.Equal("Alpha", result[0].Name); Assert.Equal("Zebra", result[1].Name); } [Fact] public async Task EnsureDefaultCategoriesAsync_CreatesDefaults_WhenEmpty() { using var factory = new TestDbContextFactory(); using var context = factory.CreateContext(); var service = new CategoryService(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); var categories = await verifyCtx.Categories .Where(c => c.UserId == TestDbContextFactory.TestUserId) .ToListAsync(); Assert.Equal(18, categories.Count); Assert.Contains(categories, c => c.Name == "Groceries" && c.Icon == "🛒"); Assert.Contains(categories, c => c.Name == "Transport" && c.Icon == "🚗"); Assert.Contains(categories, c => c.Name == "Salary" && c.Icon == "💰"); } [Fact] public async Task EnsureDefaultCategoriesAsync_DoesNotDuplicate_WhenCalledAgain() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var service = new CategoryService(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } using (var ctx = factory.CreateContext()) { var service = new CategoryService(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } using var verifyCtx = factory.CreateContext(); var count = await verifyCtx.Categories .Where(c => c.UserId == TestDbContextFactory.TestUserId) .CountAsync(); Assert.Equal(18, count); } [Fact] public async Task EnsureDefaultCategoriesAsync_BackfillsIcons_WhenMissing() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { ctx.Categories.Add(new Category { Name = "Groceries", Icon = "", UserId = TestDbContextFactory.TestUserId }); await ctx.SaveChangesAsync(); } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); var category = await verifyCtx.Categories .SingleAsync(c => c.Name == "Groceries" && c.UserId == TestDbContextFactory.TestUserId); Assert.Equal("🛒", category.Icon); } [Fact] public async Task EnsureDefaultCategoriesAsync_DoesNotOverwrite_ExistingIcons() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { ctx.Categories.Add(new Category { Name = "Groceries", Icon = "🥕", UserId = TestDbContextFactory.TestUserId }); await ctx.SaveChangesAsync(); } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); var category = await verifyCtx.Categories .SingleAsync(c => c.Name == "Groceries" && c.UserId == TestDbContextFactory.TestUserId); Assert.Equal("🥕", category.Icon); } [Fact] public async Task EnsureDefaultCategoriesAsync_IsolatedPerUser() { using var factory = new TestDbContextFactory(); using (var ctx = factory.CreateContext()) { var service = new CategoryService(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId); } using (var ctx = factory.CreateContext()) { var service = new CategoryService(ctx); await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.OtherUserId); } using var verifyCtx = factory.CreateContext(); var totalCount = await verifyCtx.Categories.CountAsync(); Assert.Equal(36, totalCount); } [Fact] public async Task CreateCategoryAsync_AddsToDatabase() { using var factory = new TestDbContextFactory(); using var context = factory.CreateContext(); var service = new CategoryService(context); await service.CreateCategoryAsync(new Category { Name = "Custom", Icon = "🎯", UserId = TestDbContextFactory.TestUserId }); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Categories); } [Fact] public async Task UpdateCategoryAsync_UpdatesNameAndIcon() { using var factory = new TestDbContextFactory(); int categoryId; using (var ctx = factory.CreateContext()) { var category = new Category { Name = "Old", Icon = "📁", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); await ctx.SaveChangesAsync(); categoryId = category.Id; } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.TestUserId, "New", "🎯"); using var verifyCtx = factory.CreateContext(); var updated = verifyCtx.Categories.Single(); Assert.Equal("New", updated.Name); Assert.Equal("🎯", updated.Icon); } [Fact] public async Task UpdateCategoryAsync_WrongUser_DoesNothing() { using var factory = new TestDbContextFactory(); int categoryId; using (var ctx = factory.CreateContext()) { var category = new Category { Name = "Original", Icon = "📁", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); await ctx.SaveChangesAsync(); categoryId = category.Id; } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.OtherUserId, "Hacked", "💀"); using var verifyCtx = factory.CreateContext(); Assert.Equal("Original", verifyCtx.Categories.Single().Name); } [Fact] public async Task DeleteCategoryAsync_RemovesCategory() { using var factory = new TestDbContextFactory(); int categoryId; using (var ctx = factory.CreateContext()) { var category = new Category { Name = "ToDelete", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); await ctx.SaveChangesAsync(); categoryId = category.Id; } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.TestUserId); using var verifyCtx = factory.CreateContext(); Assert.Empty(verifyCtx.Categories); } [Fact] public async Task DeleteCategoryAsync_WrongUser_DoesNothing() { using var factory = new TestDbContextFactory(); int categoryId; using (var ctx = factory.CreateContext()) { var category = new Category { Name = "Protected", UserId = TestDbContextFactory.TestUserId }; ctx.Categories.Add(category); await ctx.SaveChangesAsync(); categoryId = category.Id; } using var context = factory.CreateContext(); var service = new CategoryService(context); await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.OtherUserId); using var verifyCtx = factory.CreateContext(); Assert.Single(verifyCtx.Categories); } }