using Application.Dto.Icon; using Service.IconSearch; using WebApi.Controllers; namespace WebApi.Test.Controllers; /// /// IconController 集成测试 /// public class IconControllerTest : BaseTest { private readonly IconController _controller; private readonly IIconSearchService _iconSearchService; private readonly ILogger _logger; public IconControllerTest() { _iconSearchService = Substitute.For(); _logger = Substitute.For>(); _controller = new IconController(_iconSearchService, _logger); } #region POST /api/icons/search-keywords Tests [Fact] public async Task GenerateSearchKeywords_应该返回成功响应() { // Arrange var request = new SearchKeywordsRequest { CategoryName = "餐饮" }; var expectedKeywords = new List { "food", "restaurant", "dining" }; _iconSearchService.GenerateSearchKeywordsAsync(request.CategoryName) .Returns(Task.FromResult(expectedKeywords)); // Act var response = await _controller.GenerateSearchKeywordsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Data.Should().NotBeNull(); response.Data!.Keywords.Should().BeEquivalentTo(expectedKeywords); await _iconSearchService.Received(1).GenerateSearchKeywordsAsync(request.CategoryName); } [Fact] public async Task GenerateSearchKeywords_应该处理空关键字列表() { // Arrange var request = new SearchKeywordsRequest { CategoryName = "未知分类" }; _iconSearchService.GenerateSearchKeywordsAsync(request.CategoryName) .Returns(Task.FromResult(new List())); // Act var response = await _controller.GenerateSearchKeywordsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Data.Should().NotBeNull(); response.Data!.Keywords.Should().BeEmpty(); } [Theory] [InlineData("餐饮")] [InlineData("交通")] [InlineData("购物")] [InlineData("医疗")] public async Task GenerateSearchKeywords_应该处理不同的分类名称(string categoryName) { // Arrange var request = new SearchKeywordsRequest { CategoryName = categoryName }; var keywords = new List { "test1", "test2" }; _iconSearchService.GenerateSearchKeywordsAsync(categoryName) .Returns(Task.FromResult(keywords)); // Act var response = await _controller.GenerateSearchKeywordsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Data!.Keywords.Should().HaveCount(2); } [Fact] public async Task GenerateSearchKeywords_应该处理服务层异常() { // Arrange var request = new SearchKeywordsRequest { CategoryName = "测试" }; _iconSearchService.GenerateSearchKeywordsAsync(request.CategoryName) .Returns>(_ => throw new Exception("服务异常")); // Act & Assert await FluentActions.Invoking(() => _controller.GenerateSearchKeywordsAsync(request)) .Should().ThrowAsync() .WithMessage("服务异常"); } #endregion #region POST /api/icons/search Tests [Fact] public async Task SearchIcons_应该返回图标候选列表() { // Arrange var request = new SearchIconsRequest { Keywords = new List { "food", "restaurant" } }; var icons = new List { new() { CollectionName = "mdi", IconName = "food" }, new() { CollectionName = "mdi", IconName = "restaurant" }, new() { CollectionName = "fa", IconName = "utensils" } }; _iconSearchService.SearchIconsAsync(request.Keywords, 20) .Returns(Task.FromResult(icons)); // Act var response = await _controller.SearchIconsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Data.Should().NotBeNull(); response.Data!.Count.Should().Be(3); response.Data[0].IconIdentifier.Should().Be("mdi:food"); response.Data[1].IconIdentifier.Should().Be("mdi:restaurant"); response.Data[2].IconIdentifier.Should().Be("fa:utensils"); await _iconSearchService.Received(1).SearchIconsAsync(request.Keywords, 20); } [Fact] public async Task SearchIcons_应该处理空结果() { // Arrange var request = new SearchIconsRequest { Keywords = new List { "nonexistent" } }; _iconSearchService.SearchIconsAsync(request.Keywords, 20) .Returns(Task.FromResult(new List())); // Act var response = await _controller.SearchIconsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Data.Should().NotBeNull(); response.Data!.Should().BeEmpty(); } [Fact] public async Task SearchIcons_应该正确映射IconCandidate到IconCandidateDto() { // Arrange var request = new SearchIconsRequest { Keywords = new List { "home" } }; var icons = new List { new() { CollectionName = "mdi", IconName = "home" } }; _iconSearchService.SearchIconsAsync(request.Keywords, 20) .Returns(Task.FromResult(icons)); // Act var response = await _controller.SearchIconsAsync(request); // Assert response.Data!.Count.Should().Be(1); var dto = response.Data[0]; dto.CollectionName.Should().Be("mdi"); dto.IconName.Should().Be("home"); dto.IconIdentifier.Should().Be("mdi:home"); } [Fact] public async Task SearchIcons_应该处理多个关键字() { // Arrange var keywords = new List { "food", "restaurant", "dining", "eat", "meal" }; var request = new SearchIconsRequest { Keywords = keywords }; var icons = new List { new() { CollectionName = "mdi", IconName = "food" } }; _iconSearchService.SearchIconsAsync(keywords, 20) .Returns(Task.FromResult(icons)); // Act var response = await _controller.SearchIconsAsync(request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); await _iconSearchService.Received(1).SearchIconsAsync(keywords, 20); } [Fact] public async Task SearchIcons_应该使用固定的Limit值20() { // Arrange var request = new SearchIconsRequest { Keywords = new List { "test" } }; _iconSearchService.SearchIconsAsync(request.Keywords, 20) .Returns(Task.FromResult(new List())); // Act await _controller.SearchIconsAsync(request); // Assert // 验证使用的是固定的 limit=20 await _iconSearchService.Received(1).SearchIconsAsync(request.Keywords, 20); } #endregion #region PUT /api/icons/categories/{categoryId}/icon Tests [Fact] public async Task UpdateCategoryIcon_应该返回成功响应() { // Arrange const long categoryId = 12345L; var request = new UpdateCategoryIconRequest { IconIdentifier = "mdi:food" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier) .Returns(Task.CompletedTask); // Act var response = await _controller.UpdateCategoryIconAsync(categoryId, request); // Assert response.Should().NotBeNull(); response.Success.Should().BeTrue(); response.Message.Should().Be("更新分类图标成功"); await _iconSearchService.Received(1).UpdateCategoryIconAsync(categoryId, request.IconIdentifier); } [Fact] public async Task UpdateCategoryIcon_应该处理不同的分类ID() { // Arrange const long categoryId = 99999L; var request = new UpdateCategoryIconRequest { IconIdentifier = "fa:shopping-cart" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier) .Returns(Task.CompletedTask); // Act var response = await _controller.UpdateCategoryIconAsync(categoryId, request); // Assert response.Success.Should().BeTrue(); await _iconSearchService.Received(1).UpdateCategoryIconAsync(categoryId, request.IconIdentifier); } [Fact] public async Task UpdateCategoryIcon_应该处理不同的图标标识符() { // Arrange const long categoryId = 12345L; var request = new UpdateCategoryIconRequest { IconIdentifier = "tabler:airplane" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier) .Returns(Task.CompletedTask); // Act var response = await _controller.UpdateCategoryIconAsync(categoryId, request); // Assert response.Success.Should().BeTrue(); await _iconSearchService.Received(1).UpdateCategoryIconAsync(categoryId, request.IconIdentifier); } [Fact] public async Task UpdateCategoryIcon_应该处理分类不存在异常() { // Arrange const long categoryId = 99999L; var request = new UpdateCategoryIconRequest { IconIdentifier = "mdi:food" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier) .Returns(_ => throw new Exception("分类不存在")); // Act & Assert await FluentActions.Invoking(() => _controller.UpdateCategoryIconAsync(categoryId, request)) .Should().ThrowAsync() .WithMessage("分类不存在"); } [Fact] public async Task UpdateCategoryIcon_应该处理无效图标标识符异常() { // Arrange const long categoryId = 12345L; var request = new UpdateCategoryIconRequest { IconIdentifier = "" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier) .Returns(_ => throw new ArgumentException("图标标识符不能为空")); // Act & Assert await FluentActions.Invoking(() => _controller.UpdateCategoryIconAsync(categoryId, request)) .Should().ThrowAsync() .WithMessage("*图标标识符不能为空*"); } #endregion #region Integration Flow Tests [Fact] public async Task IntegrationFlow_完整流程_生成关键字到搜索到更新() { // Arrange - Step 1: 生成搜索关键字 var keywordsRequest = new SearchKeywordsRequest { CategoryName = "餐饮" }; var keywords = new List { "food", "restaurant" }; _iconSearchService.GenerateSearchKeywordsAsync(keywordsRequest.CategoryName) .Returns(Task.FromResult(keywords)); // Arrange - Step 2: 搜索图标 var searchRequest = new SearchIconsRequest { Keywords = keywords }; var icons = new List { new() { CollectionName = "mdi", IconName = "food" }, new() { CollectionName = "mdi", IconName = "restaurant" } }; _iconSearchService.SearchIconsAsync(keywords, 20) .Returns(Task.FromResult(icons)); // Arrange - Step 3: 更新分类图标 const long categoryId = 12345L; var updateRequest = new UpdateCategoryIconRequest { IconIdentifier = "mdi:food" }; _iconSearchService.UpdateCategoryIconAsync(categoryId, updateRequest.IconIdentifier) .Returns(Task.CompletedTask); // Act - Step 1: 生成关键字 var keywordsResponse = await _controller.GenerateSearchKeywordsAsync(keywordsRequest); // Assert - Step 1 keywordsResponse.Success.Should().BeTrue(); keywordsResponse.Data!.Keywords.Should().BeEquivalentTo(keywords); // Act - Step 2: 搜索图标 var searchResponse = await _controller.SearchIconsAsync(searchRequest); // Assert - Step 2 searchResponse.Success.Should().BeTrue(); searchResponse.Data!.Count.Should().Be(2); // Act - Step 3: 更新分类图标 var updateResponse = await _controller.UpdateCategoryIconAsync(categoryId, updateRequest); // Assert - Step 3 updateResponse.Success.Should().BeTrue(); // 验证所有服务调用 await _iconSearchService.Received(1).GenerateSearchKeywordsAsync(keywordsRequest.CategoryName); await _iconSearchService.Received(1).SearchIconsAsync(keywords, 20); await _iconSearchService.Received(1).UpdateCategoryIconAsync(categoryId, updateRequest.IconIdentifier); } [Fact] public async Task IntegrationFlow_部分失败_关键字生成返回空() { // Arrange var keywordsRequest = new SearchKeywordsRequest { CategoryName = "未知" }; _iconSearchService.GenerateSearchKeywordsAsync(keywordsRequest.CategoryName) .Returns(Task.FromResult(new List())); // Act - Step 1: 生成关键字 var keywordsResponse = await _controller.GenerateSearchKeywordsAsync(keywordsRequest); // Assert keywordsResponse.Success.Should().BeTrue(); keywordsResponse.Data!.Keywords.Should().BeEmpty(); // 空关键字列表仍然可以传递给搜索接口,但通常会返回空结果 var searchRequest = new SearchIconsRequest { Keywords = keywordsResponse.Data.Keywords }; _iconSearchService.SearchIconsAsync(searchRequest.Keywords, 20) .Returns(Task.FromResult(new List())); var searchResponse = await _controller.SearchIconsAsync(searchRequest); searchResponse.Success.Should().BeTrue(); searchResponse.Data!.Should().BeEmpty(); } #endregion #region DTO Validation Tests [Fact] public async Task SearchKeywordsRequest_应该包含CategoryName字段() { // Arrange var request = new SearchKeywordsRequest { CategoryName = "测试" }; // Assert request.CategoryName.Should().Be("测试"); } [Fact] public async Task SearchIconsRequest_应该包含Keywords字段() { // Arrange var keywords = new List { "test1", "test2" }; var request = new SearchIconsRequest { Keywords = keywords }; // Assert request.Keywords.Should().BeEquivalentTo(keywords); } [Fact] public async Task UpdateCategoryIconRequest_应该包含IconIdentifier字段() { // Arrange var request = new UpdateCategoryIconRequest { IconIdentifier = "mdi:test" }; // Assert request.IconIdentifier.Should().Be("mdi:test"); } [Fact] public async Task IconCandidateDto_应该包含所有必需字段() { // Arrange var dto = new IconCandidateDto { CollectionName = "mdi", IconName = "food", IconIdentifier = "mdi:food" }; // Assert dto.CollectionName.Should().Be("mdi"); dto.IconName.Should().Be("food"); dto.IconIdentifier.Should().Be("mdi:food"); } #endregion }