264 lines
11 KiB
C#
264 lines
11 KiB
C#
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
|
|
}
|