Files
EmailBill/WebApi.Test/Controllers/IconControllerTest.cs

444 lines
15 KiB
C#
Raw Normal View History

using Application.Dto.Icon;
using Service.IconSearch;
using WebApi.Controllers;
namespace WebApi.Test.Controllers;
/// <summary>
/// IconController 集成测试
/// </summary>
public class IconControllerTest : BaseTest
{
private readonly IconController _controller;
private readonly IIconSearchService _iconSearchService;
private readonly ILogger<IconController> _logger;
public IconControllerTest()
{
_iconSearchService = Substitute.For<IIconSearchService>();
_logger = Substitute.For<ILogger<IconController>>();
_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<string> { "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<string>()));
// 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<string> { "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<List<string>>(_ => throw new Exception("服务异常"));
// Act & Assert
await FluentActions.Invoking(() => _controller.GenerateSearchKeywordsAsync(request))
.Should().ThrowAsync<Exception>()
.WithMessage("服务异常");
}
#endregion
#region POST /api/icons/search Tests
[Fact]
public async Task SearchIcons_应该返回图标候选列表()
{
// Arrange
var request = new SearchIconsRequest { Keywords = new List<string> { "food", "restaurant" } };
var icons = new List<IconCandidate>
{
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<string> { "nonexistent" } };
_iconSearchService.SearchIconsAsync(request.Keywords, 20)
.Returns(Task.FromResult(new List<IconCandidate>()));
// 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<string> { "home" } };
var icons = new List<IconCandidate>
{
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<string> { "food", "restaurant", "dining", "eat", "meal" };
var request = new SearchIconsRequest { Keywords = keywords };
var icons = new List<IconCandidate>
{
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<string> { "test" } };
_iconSearchService.SearchIconsAsync(request.Keywords, 20)
.Returns(Task.FromResult(new List<IconCandidate>()));
// 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<Task>(_ => throw new Exception("分类不存在"));
// Act & Assert
await FluentActions.Invoking(() => _controller.UpdateCategoryIconAsync(categoryId, request))
.Should().ThrowAsync<Exception>()
.WithMessage("分类不存在");
}
[Fact]
public async Task UpdateCategoryIcon_应该处理无效图标标识符异常()
{
// Arrange
const long categoryId = 12345L;
var request = new UpdateCategoryIconRequest { IconIdentifier = "" };
_iconSearchService.UpdateCategoryIconAsync(categoryId, request.IconIdentifier)
.Returns<Task>(_ => throw new ArgumentException("图标标识符不能为空"));
// Act & Assert
await FluentActions.Invoking(() => _controller.UpdateCategoryIconAsync(categoryId, request))
.Should().ThrowAsync<ArgumentException>()
.WithMessage("*图标标识符不能为空*");
}
#endregion
#region Integration Flow Tests
[Fact]
public async Task IntegrationFlow_完整流程_生成关键字到搜索到更新()
{
// Arrange - Step 1: 生成搜索关键字
var keywordsRequest = new SearchKeywordsRequest { CategoryName = "餐饮" };
var keywords = new List<string> { "food", "restaurant" };
_iconSearchService.GenerateSearchKeywordsAsync(keywordsRequest.CategoryName)
.Returns(Task.FromResult(keywords));
// Arrange - Step 2: 搜索图标
var searchRequest = new SearchIconsRequest { Keywords = keywords };
var icons = new List<IconCandidate>
{
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<string>()));
// 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<IconCandidate>()));
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<string> { "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
}