Files
EmailBill/WebApi.Test/Service/SmartHandleServiceTest.cs

264 lines
11 KiB
C#
Raw Normal View History

2026-02-15 10:10:28 +08:00
using NSubstitute;
using Service.AI;
using Service.Transaction;
using System.Text.Json;
namespace WebApi.Test.Service;
/// <summary>
/// SmartHandleService 单元测试
/// </summary>
public class SmartHandleServiceTest : BaseTest
{
private readonly IOpenAiService _openAiService;
private readonly ITransactionRecordRepository _transactionRecordRepository;
private readonly ITransactionStatisticsService _transactionStatisticsService;
private readonly ITextSegmentService _textSegmentService;
private readonly ILogger<SmartHandleService> _logger;
private readonly ITransactionCategoryRepository _categoryRepository;
private readonly IConfigService _configService;
private readonly IClassificationIconPromptProvider _iconPromptProvider;
private readonly SmartHandleService _service;
public SmartHandleServiceTest()
{
_openAiService = Substitute.For<IOpenAiService>();
_transactionRecordRepository = Substitute.For<ITransactionRecordRepository>();
_transactionStatisticsService = Substitute.For<ITransactionStatisticsService>();
_textSegmentService = Substitute.For<ITextSegmentService>();
_logger = Substitute.For<ILogger<SmartHandleService>>();
_categoryRepository = Substitute.For<ITransactionCategoryRepository>();
_configService = Substitute.For<IConfigService>();
_iconPromptProvider = Substitute.For<IClassificationIconPromptProvider>();
_service = new SmartHandleService(
_transactionRecordRepository,
_transactionStatisticsService,
_textSegmentService,
_logger,
_categoryRepository,
_openAiService,
_configService,
_iconPromptProvider
);
}
#region GenerateSingleCategoryIconAsync Tests
[Fact]
public async Task GenerateSingleCategoryIconAsync_AI返回带Markdown标记的JSON_应成功解析()
{
// Arrange
const string categoryName = "餐饮";
const TransactionType categoryType = TransactionType.Expense;
const string aiResponseWithMarkdown = """
```json
["<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>"]
```
""";
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(aiResponseWithMarkdown);
// Act
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
// Assert
result.Should().NotBeNull();
result.Should().Contain("<svg");
result.Should().Contain("viewBox=\"0 0 24 24\"");
}
[Fact]
public async Task GenerateSingleCategoryIconAsync_AI返回纯JSON_应成功解析()
{
// Arrange
const string categoryName = "交通";
const TransactionType categoryType = TransactionType.Expense;
const string aiResponsePureJson = """
["<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"5\" width=\"14\" height=\"14\" fill=\"#4a90e2\"/></svg>"]
""";
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(aiResponsePureJson);
// Act
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
// Assert
result.Should().NotBeNull();
result.Should().Contain("<svg");
}
[Fact]
public async Task GenerateSingleCategoryIconAsync_AI返回空响应_应返回null()
{
// Arrange
const string categoryName = "购物";
const TransactionType categoryType = TransactionType.Expense;
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns((string?)null);
// Act
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GenerateSingleCategoryIconAsync_AI返回无效JSON_应返回null()
{
// Arrange
const string categoryName = "娱乐";
const TransactionType categoryType = TransactionType.Expense;
const string invalidJsonResponse = "这是一个文本响应,不是 JSON";
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(invalidJsonResponse);
// Act
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
// Assert
result.Should().BeNull();
}
#endregion
#region GenerateCategoryIconsAsync Tests
[Fact]
public async Task GenerateCategoryIconsAsync_AI返回带Markdown标记的JSON_应成功解析()
{
// Arrange
const string categoryName = "餐饮";
const TransactionType categoryType = TransactionType.Expense;
const int iconCount = 5;
const string aiResponseWithMarkdown = """
```json
[
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"12\" rx=\"10\" ry=\"6\" fill=\"#6c5ce7\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><polygon points=\"12,2 22,8 22,16 12,22 2,16 2,8\" fill=\"#a29bfe\"/></svg>"
]
```
""";
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(aiResponseWithMarkdown);
// Act
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(iconCount);
result.Should().AllSatisfy(icon => icon.Should().Contain("<svg"));
}
[Fact]
public async Task GenerateCategoryIconsAsync_AI返回纯JSON_应成功解析()
{
// Arrange
const string categoryName = "交通";
const TransactionType categoryType = TransactionType.Expense;
const int iconCount = 5;
const string aiResponsePureJson = """
[
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"12\" rx=\"10\" ry=\"6\" fill=\"#6c5ce7\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><polygon points=\"12,2 22,8 22,16 12,22 2,16 2,8\" fill=\"#a29bfe\"/></svg>"
]
""";
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(aiResponsePureJson);
// Act
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(iconCount);
}
[Fact]
public async Task GenerateCategoryIconsAsync_AI返回空响应_应返回null()
{
// Arrange
const string categoryName = "购物";
const TransactionType categoryType = TransactionType.Expense;
const int iconCount = 5;
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns((string?)null);
// Act
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GenerateCategoryIconsAsync_AI返回图标数量不正确_应返回null()
{
// Arrange
const string categoryName = "娱乐";
const TransactionType categoryType = TransactionType.Expense;
const int iconCount = 5;
const string aiResponseWith3Icons = """
[
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>"
]
""";
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(aiResponseWith3Icons);
// Act
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GenerateCategoryIconsAsync_AI返回无效JSON_应返回null()
{
// Arrange
const string categoryName = "其他";
const TransactionType categoryType = TransactionType.Expense;
const int iconCount = 5;
const string invalidJsonResponse = "这是一个文本响应,不是 JSON";
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
.Returns(invalidJsonResponse);
// Act
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
// Assert
result.Should().BeNull();
}
#endregion
}