using Application.Dto.Category; using Service.AI; namespace WebApi.Test.Application; /// /// TransactionCategoryApplication 单元测试 /// public class TransactionCategoryApplicationTest : BaseApplicationTest { private readonly ITransactionCategoryRepository _categoryRepository; private readonly ITransactionRecordRepository _transactionRepository; private readonly IBudgetRepository _budgetRepository; private readonly ISmartHandleService _smartHandleService; private readonly ILogger _logger; private readonly TransactionCategoryApplication _application; public TransactionCategoryApplicationTest() { _categoryRepository = Substitute.For(); _transactionRepository = Substitute.For(); _budgetRepository = Substitute.For(); _smartHandleService = Substitute.For(); _logger = CreateMockLogger(); _application = new TransactionCategoryApplication( _categoryRepository, _transactionRepository, _budgetRepository, _smartHandleService, _logger); } #region GetListAsync Tests [Fact] public async Task GetListAsync_无类型筛选_应返回所有分类() { // Arrange var categories = new List { new() { Id = 1, Name = "餐饮", Type = TransactionType.Expense }, new() { Id = 2, Name = "工资", Type = TransactionType.Income } }; _categoryRepository.GetAllAsync().Returns(categories); // Act var result = await _application.GetListAsync(); // Assert result.Should().HaveCount(2); result.Should().Contain(c => c.Name == "餐饮"); result.Should().Contain(c => c.Name == "工资"); } [Fact] public async Task GetListAsync_指定类型_应返回该类型分类() { // Arrange var expenseCategories = new List { new() { Id = 1, Name = "餐饮", Type = TransactionType.Expense }, new() { Id = 2, Name = "交通", Type = TransactionType.Expense } }; _categoryRepository.GetCategoriesByTypeAsync(TransactionType.Expense).Returns(expenseCategories); // Act var result = await _application.GetListAsync(TransactionType.Expense); // Assert result.Should().HaveCount(2); result.Should().AllSatisfy(c => c.Type.Should().Be(TransactionType.Expense)); } #endregion #region GetByIdAsync Tests [Fact] public async Task GetByIdAsync_存在的分类_应返回分类详情() { // Arrange var category = new TransactionCategory { Id = 1, Name = "餐饮", Type = TransactionType.Expense }; _categoryRepository.GetByIdAsync(1).Returns(category); // Act var result = await _application.GetByIdAsync(1); // Assert result.Should().NotBeNull(); result.Id.Should().Be(1); result.Name.Should().Be("餐饮"); } [Fact] public async Task GetByIdAsync_不存在的分类_应抛出NotFoundException() { // Arrange _categoryRepository.GetByIdAsync(999).Returns((TransactionCategory?)null); // Act & Assert await Assert.ThrowsAsync(() => _application.GetByIdAsync(999)); } #endregion #region CreateAsync Tests [Fact] public async Task CreateAsync_有效请求_应成功创建() { // Arrange var request = new CreateCategoryRequest { Name = "新分类", Type = TransactionType.Expense }; _categoryRepository.GetByNameAndTypeAsync("新分类", TransactionType.Expense).Returns((TransactionCategory?)null); _categoryRepository.AddAsync(Arg.Any()).Returns(true); // Act var result = await _application.CreateAsync(request); // Assert result.Should().BeGreaterThan(0); await _categoryRepository.Received(1).AddAsync(Arg.Is( c => c.Name == "新分类" && c.Type == TransactionType.Expense )); } [Fact] public async Task CreateAsync_同名分类已存在_应抛出ValidationException() { // Arrange var existingCategory = new TransactionCategory { Id = 1, Name = "餐饮", Type = TransactionType.Expense }; var request = new CreateCategoryRequest { Name = "餐饮", Type = TransactionType.Expense }; _categoryRepository.GetByNameAndTypeAsync("餐饮", TransactionType.Expense).Returns(existingCategory); // Act & Assert var exception = await Assert.ThrowsAsync(() => _application.CreateAsync(request)); exception.Message.Should().Contain("已存在相同名称的分类"); } [Fact] public async Task CreateAsync_Repository添加失败_应抛出BusinessException() { // Arrange var request = new CreateCategoryRequest { Name = "测试分类", Type = TransactionType.Expense }; _categoryRepository.GetByNameAndTypeAsync("测试分类", TransactionType.Expense).Returns((TransactionCategory?)null); _categoryRepository.AddAsync(Arg.Any()).Returns(false); // Act & Assert await Assert.ThrowsAsync(() => _application.CreateAsync(request)); } #endregion #region UpdateAsync Tests [Fact] public async Task UpdateAsync_有效请求_应成功更新() { // Arrange var existingCategory = new TransactionCategory { Id = 1, Name = "旧名称", Type = TransactionType.Expense }; var request = new UpdateCategoryRequest { Id = 1, Name = "新名称" }; _categoryRepository.GetByIdAsync(1).Returns(existingCategory); _categoryRepository.GetByNameAndTypeAsync("新名称", TransactionType.Expense).Returns((TransactionCategory?)null); _categoryRepository.UpdateAsync(Arg.Any()).Returns(true); // Act await _application.UpdateAsync(request); // Assert await _categoryRepository.Received(1).UpdateAsync(Arg.Is( c => c.Id == 1 && c.Name == "新名称" )); await _transactionRepository.Received(1).UpdateCategoryNameAsync("旧名称", "新名称", TransactionType.Expense); await _budgetRepository.Received(1).UpdateBudgetCategoryNameAsync("旧名称", "新名称", TransactionType.Expense); } [Fact] public async Task UpdateAsync_分类不存在_应抛出NotFoundException() { // Arrange var request = new UpdateCategoryRequest { Id = 999, Name = "新名称" }; _categoryRepository.GetByIdAsync(999).Returns((TransactionCategory?)null); // Act & Assert await Assert.ThrowsAsync(() => _application.UpdateAsync(request)); } [Fact] public async Task UpdateAsync_新名称已存在_应抛出ValidationException() { // Arrange var existingCategory = new TransactionCategory { Id = 1, Name = "旧名称", Type = TransactionType.Expense }; var conflictingCategory = new TransactionCategory { Id = 2, Name = "冲突名称", Type = TransactionType.Expense }; var request = new UpdateCategoryRequest { Id = 1, Name = "冲突名称" }; _categoryRepository.GetByIdAsync(1).Returns(existingCategory); _categoryRepository.GetByNameAndTypeAsync("冲突名称", TransactionType.Expense).Returns(conflictingCategory); // Act & Assert await Assert.ThrowsAsync(() => _application.UpdateAsync(request)); } [Fact] public async Task UpdateAsync_名称未改变_不应同步更新关联数据() { // Arrange var existingCategory = new TransactionCategory { Id = 1, Name = "相同名称", Type = TransactionType.Expense }; var request = new UpdateCategoryRequest { Id = 1, Name = "相同名称" }; _categoryRepository.GetByIdAsync(1).Returns(existingCategory); _categoryRepository.UpdateAsync(Arg.Any()).Returns(true); // Act await _application.UpdateAsync(request); // Assert await _transactionRepository.DidNotReceive().UpdateCategoryNameAsync(Arg.Any(), Arg.Any(), Arg.Any()); await _budgetRepository.DidNotReceive().UpdateBudgetCategoryNameAsync(Arg.Any(), Arg.Any(), Arg.Any()); } #endregion #region DeleteAsync Tests [Fact] public async Task DeleteAsync_未被使用的分类_应成功删除() { // Arrange _categoryRepository.IsCategoryInUseAsync(1).Returns(false); _categoryRepository.DeleteAsync(1).Returns(true); // Act await _application.DeleteAsync(1); // Assert await _categoryRepository.Received(1).DeleteAsync(1); } [Fact] public async Task DeleteAsync_已被使用的分类_应抛出ValidationException() { // Arrange _categoryRepository.IsCategoryInUseAsync(1).Returns(true); // Act & Assert var exception = await Assert.ThrowsAsync(() => _application.DeleteAsync(1)); exception.Message.Should().Contain("已被使用"); } [Fact] public async Task DeleteAsync_删除失败_应抛出BusinessException() { // Arrange _categoryRepository.IsCategoryInUseAsync(1).Returns(false); _categoryRepository.DeleteAsync(1).Returns(false); // Act & Assert await Assert.ThrowsAsync(() => _application.DeleteAsync(1)); } #endregion #region BatchCreateAsync Tests [Fact] public async Task BatchCreateAsync_有效请求列表_应返回创建数量() { // Arrange var requests = new List { new() { Name = "分类1", Type = TransactionType.Expense }, new() { Name = "分类2", Type = TransactionType.Expense } }; _categoryRepository.AddRangeAsync(Arg.Any>()).Returns(true); // Act var result = await _application.BatchCreateAsync(requests); // Assert result.Should().Be(2); await _categoryRepository.Received(1).AddRangeAsync(Arg.Is>( list => list.Count == 2 )); } [Fact] public async Task BatchCreateAsync_Repository添加失败_应抛出BusinessException() { // Arrange var requests = new List { new() { Name = "分类1", Type = TransactionType.Expense } }; _categoryRepository.AddRangeAsync(Arg.Any>()).Returns(false); // Act & Assert await Assert.ThrowsAsync(() => _application.BatchCreateAsync(requests)); } #endregion #region UpdateSelectedIconAsync Tests [Fact] public async Task UpdateSelectedIconAsync_有效索引_应更新选中图标() { // Arrange var category = new TransactionCategory { Id = 1, Name = "测试", Type = TransactionType.Expense, Icon = """["icon1","icon2","icon3"]""" }; var request = new UpdateSelectedIconRequest { CategoryId = 1, SelectedIndex = 2 }; _categoryRepository.GetByIdAsync(1).Returns(category); _categoryRepository.UpdateAsync(Arg.Any()).Returns(true); // Act await _application.UpdateSelectedIconAsync(request); // Assert await _categoryRepository.Received(1).UpdateAsync(Arg.Is( c => c.Id == 1 && c.Icon != null && c.Icon.Contains("icon3") )); } [Fact] public async Task UpdateSelectedIconAsync_分类不存在_应抛出NotFoundException() { // Arrange var request = new UpdateSelectedIconRequest { CategoryId = 999, SelectedIndex = 0 }; _categoryRepository.GetByIdAsync(999).Returns((TransactionCategory?)null); // Act & Assert await Assert.ThrowsAsync(() => _application.UpdateSelectedIconAsync(request)); } [Fact] public async Task UpdateSelectedIconAsync_分类无图标_应抛出ValidationException() { // Arrange var category = new TransactionCategory { Id = 1, Name = "测试", Type = TransactionType.Expense, Icon = null }; var request = new UpdateSelectedIconRequest { CategoryId = 1, SelectedIndex = 0 }; _categoryRepository.GetByIdAsync(1).Returns(category); // Act & Assert await Assert.ThrowsAsync(() => _application.UpdateSelectedIconAsync(request)); } [Fact] public async Task UpdateSelectedIconAsync_索引超出范围_应抛出ValidationException() { // Arrange var category = new TransactionCategory { Id = 1, Name = "测试", Type = TransactionType.Expense, Icon = """["icon1"]""" }; var request = new UpdateSelectedIconRequest { CategoryId = 1, SelectedIndex = 5 }; _categoryRepository.GetByIdAsync(1).Returns(category); // Act & Assert await Assert.ThrowsAsync(() => _application.UpdateSelectedIconAsync(request)); } #endregion }