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 TransactionCategoryApplication _application;
public TransactionCategoryApplicationTest()
{
_categoryRepository = Substitute.For();
_transactionRepository = Substitute.For();
_budgetRepository = Substitute.For();
_smartHandleService = Substitute.For();
_application = new TransactionCategoryApplication(
_categoryRepository,
_transactionRepository,
_budgetRepository,
_smartHandleService);
}
#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 = """["","",""]"""
};
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 = """[""]"""
};
var request = new UpdateSelectedIconRequest
{
CategoryId = 1,
SelectedIndex = 5
};
_categoryRepository.GetByIdAsync(1).Returns(category);
// Act & Assert
await Assert.ThrowsAsync(() => _application.UpdateSelectedIconAsync(request));
}
#endregion
}