Files
EmailBill/WebApi.Test/Service/IconSearch/IconifyApiServiceTest.cs
SunCheng 9921cd5fdf chore: migrate remaining ECharts components to Chart.js
- Migrated 4 components from ECharts to Chart.js:
  * MonthlyExpenseCard.vue (折线图)
  * DailyTrendChart.vue (双系列折线图)
  * ExpenseCategoryCard.vue (环形图)
  * BudgetChartAnalysis.vue (仪表盘 + 多种图表)

- Removed all ECharts imports and environment variable switches
- Unified all charts to use BaseChart.vue component
- Build verified: pnpm build success ✓
- No echarts imports remaining ✓

Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
2026-02-16 21:55:38 +08:00

390 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.IconSearch;
using System.Text.Json;
namespace WebApi.Test.Service.IconSearch;
/// <summary>
/// IconifyApiService 单元测试
/// </summary>
public class IconifyApiServiceTest : BaseTest
{
private readonly IconifyApiService _service;
private readonly IOptions<IconifySettings> _settings;
private readonly ILogger<IconifyApiService> _logger;
public IconifyApiServiceTest()
{
_logger = Substitute.For<ILogger<IconifyApiService>>();
_settings = Options.Create(new IconifySettings
{
ApiUrl = "https://api.iconify.design/search",
DefaultLimit = 20,
MaxRetryCount = 3,
RetryDelayMs = 1000
});
_service = new IconifyApiService(_settings, _logger);
}
#region Response Parsing Tests
/// <summary>
/// 测试实际的 Iconify API 响应格式
/// 实际 API 返回的 icons 是字符串数组,格式为 "collection:iconName"
/// 例如:["mdi:home", "fa:food"]
/// </summary>
[Fact]
public void IconifyApiResponse_应该正确解析实际API响应格式()
{
// Arrange - 这是从 Iconify API 实际返回的格式
var json = @"{
""icons"": [
""svg-spinners:wind-toy"",
""material-symbols:smart-toy"",
""mdi:toy-brick"",
""tabler:horse-toy""
],
""total"": 32,
""limit"": 32,
""start"": 0,
""collections"": {
""svg-spinners"": {
""name"": ""SVG Spinners"",
""total"": 46
},
""material-symbols"": {
""name"": ""Material Symbols"",
""total"": 15118
},
""mdi"": {
""name"": ""Material Design Icons"",
""total"": 7447
},
""tabler"": {
""name"": ""Tabler Icons"",
""total"": 5986
}
}
}";
// Act
var response = JsonSerializer.Deserialize<IconifyApiResponse>(json);
// Assert
response.Should().NotBeNull();
response!.Icons.Should().NotBeNull();
response.Icons!.Count.Should().Be(4);
// 验证图标格式正确解析
response.Icons[0].Should().Be("svg-spinners:wind-toy");
response.Icons[1].Should().Be("material-symbols:smart-toy");
response.Icons[2].Should().Be("mdi:toy-brick");
response.Icons[3].Should().Be("tabler:horse-toy");
}
[Fact]
public void IconifyApiResponse_旧格式测试_应该失败()
{
// Arrange - 旧的错误格式(这不是 Iconify 实际返回的)
var json = @"{
""icons"": [
{
""name"": ""home"",
""collection"": {
""name"": ""mdi""
}
},
{
""name"": ""food"",
""collection"": {
""name"": ""mdi""
}
}
]
}";
// Act & Assert - 尝试用新的正确格式解析应该抛出异常
var exception = Assert.Throws<JsonException>(() =>
{
JsonSerializer.Deserialize<IconifyApiResponse>(json);
});
// 验证异常消息包含预期的错误信息
exception.Message.Should().Contain("could not be converted to System.String");
}
[Fact]
public void IconifyApiResponse_应该处理空Icons数组()
{
// Arrange
var json = @"{ ""icons"": [] }";
// Act
var response = JsonSerializer.Deserialize<IconifyApiResponse>(json);
// Assert
response.Should().NotBeNull();
response!.Icons.Should().NotBeNull();
response.Icons!.Count.Should().Be(0);
}
[Fact]
public void IconifyApiResponse_应该处理null_Icons字段()
{
// Arrange
var json = @"{}";
// Act
var response = JsonSerializer.Deserialize<IconifyApiResponse>(json);
// Assert
response.Should().NotBeNull();
response!.Icons.Should().BeNull();
}
[Fact]
public void IconCandidate_应该正确生成IconIdentifier()
{
// Arrange
var candidate = new IconCandidate
{
CollectionName = "mdi",
IconName = "home"
};
// Act
var identifier = candidate.IconIdentifier;
// Assert
identifier.Should().Be("mdi:home");
}
[Theory]
[InlineData("mdi", "food", "mdi:food")]
[InlineData("fa", "shopping-cart", "fa:shopping-cart")]
[InlineData("tabler", "airplane", "tabler:airplane")]
[InlineData("fluent", "food-24-regular", "fluent:food-24-regular")]
public void IconCandidate_应该支持多种图标集格式(string collection, string iconName, string expected)
{
// Arrange
var candidate = new IconCandidate
{
CollectionName = collection,
IconName = iconName
};
// Act
var identifier = candidate.IconIdentifier;
// Assert
identifier.Should().Be(expected);
}
[Theory]
[InlineData("mdi:food", "mdi", "food")]
[InlineData("svg-spinners:wind-toy", "svg-spinners", "wind-toy")]
[InlineData("material-symbols:smart-toy", "material-symbols", "smart-toy")]
[InlineData("tabler:horse-toy", "tabler", "horse-toy")]
[InlineData("game-icons:toy-mallet", "game-icons", "toy-mallet")]
public void IconCandidate_应该从字符串标识符解析出CollectionName和IconName(
string iconIdentifier,
string expectedCollection,
string expectedIconName)
{
// Arrange & Act - 从 "collection:iconName" 格式解析
var parts = iconIdentifier.Split(':', 2);
var candidate = new IconCandidate
{
CollectionName = parts[0],
IconName = parts[1]
};
// Assert
candidate.CollectionName.Should().Be(expectedCollection);
candidate.IconName.Should().Be(expectedIconName);
candidate.IconIdentifier.Should().Be(iconIdentifier);
}
[Fact]
public void API响应解析IconCandidate列表()
{
// Arrange - 模拟实际 API 响应的字符串数组
var iconStrings = new List<string>
{
"svg-spinners:wind-toy",
"material-symbols:smart-toy",
"mdi:toy-brick",
"tabler:horse-toy"
};
// Act - 模拟 IconifyApiService 应该执行的解析逻辑
var candidates = iconStrings
.Select(iconStr =>
{
var parts = iconStr.Split(':', 2);
return parts.Length == 2
? new IconCandidate
{
CollectionName = parts[0],
IconName = parts[1]
}
: null;
})
.Where(c => c != null)
.ToList();
// Assert
candidates.Should().HaveCount(4);
candidates[0]!.CollectionName.Should().Be("svg-spinners");
candidates[0]!.IconName.Should().Be("wind-toy");
candidates[0]!.IconIdentifier.Should().Be("svg-spinners:wind-toy");
candidates[1]!.CollectionName.Should().Be("material-symbols");
candidates[1]!.IconName.Should().Be("smart-toy");
candidates[2]!.CollectionName.Should().Be("mdi");
candidates[2]!.IconName.Should().Be("toy-brick");
candidates[3]!.CollectionName.Should().Be("tabler");
candidates[3]!.IconName.Should().Be("horse-toy");
}
#endregion
#region Settings Tests
[Fact]
public void IconifySettings_应该使用默认值()
{
// Arrange & Act
var settings = new IconifySettings();
// Assert
settings.ApiUrl.Should().Be("https://api.iconify.design/search");
settings.DefaultLimit.Should().Be(20);
settings.MaxRetryCount.Should().Be(3);
settings.RetryDelayMs.Should().Be(1000);
}
[Fact]
public void IconifySettings_应该接受自定义配置()
{
// Arrange & Act
var settings = new IconifySettings
{
ApiUrl = "https://custom-api.example.com/search",
DefaultLimit = 50,
MaxRetryCount = 5,
RetryDelayMs = 2000
};
// Assert
settings.ApiUrl.Should().Be("https://custom-api.example.com/search");
settings.DefaultLimit.Should().Be(50);
settings.MaxRetryCount.Should().Be(5);
settings.RetryDelayMs.Should().Be(2000);
}
#endregion
#region Integration Tests ()
// 注意:以下测试需要实际的网络连接,可能会失败
// 在 CI/CD 环境中,建议使用 [Fact(Skip = "Requires network")] 或 mock HTTP 客户端
[Fact(Skip = "需要网络连接,仅用于手动测试")]
public async Task SearchIconsAsync_应该返回有效图标列表()
{
// Arrange
var keywords = new List<string> { "food", "restaurant" };
// Act
var icons = await _service.SearchIconsAsync(keywords, 10);
// Assert
icons.Should().NotBeNull();
icons.Should().NotBeEmpty();
icons.All(i => !string.IsNullOrEmpty(i.CollectionName)).Should().BeTrue();
icons.All(i => !string.IsNullOrEmpty(i.IconName)).Should().BeTrue();
icons.All(i => i.IconIdentifier.Contains(":")).Should().BeTrue();
}
[Fact(Skip = "需要网络连接,仅用于手动测试")]
public async Task SearchIconsAsync_应该处理无效关键字()
{
// Arrange
var keywords = new List<string> { "xyzabc123nonexistent" };
// Act
var icons = await _service.SearchIconsAsync(keywords, 10);
// Assert
icons.Should().NotBeNull();
// 可能返回空列表或少量图标
icons.Count.Should().BeGreaterThanOrEqualTo(0);
}
[Fact(Skip = "需要网络连接,仅用于手动测试")]
public async Task SearchIconsAsync_应该合并多个关键字的结果()
{
// Arrange
var keywords = new List<string> { "home", "house" };
// Act
var icons = await _service.SearchIconsAsync(keywords, 5);
// Assert
icons.Should().NotBeNull();
// 应该从两个关键字中获取图标
icons.Count.Should().BeGreaterThan(0);
}
[Fact]
public async Task SearchIconsAsync_应该处理空关键字列表()
{
// Arrange
var keywords = new List<string>();
// Act
var icons = await _service.SearchIconsAsync(keywords, 10);
// Assert
icons.Should().NotBeNull();
icons.Should().BeEmpty();
}
[Fact(Skip = "需要网络连接,仅用于手动测试")]
public async Task SearchIconsAsync_应该使用默认Limit()
{
// Arrange
var keywords = new List<string> { "food" };
// Act
var icons = await _service.SearchIconsAsync(keywords, 0); // limit=0 应该使用默认值20
// Assert
icons.Should().NotBeNull();
// 应该返回不超过20个图标如果API有足够的结果
icons.Count.Should().BeLessThanOrEqualTo(20);
}
#endregion
#region Error Handling Tests
[Fact]
public async Task SearchIconsAsync_应该处理部分关键字失败的情况()
{
// Arrange
var keywords = new List<string> { "food" }; // 假设这个能成功
// Act
var icons = await _service.SearchIconsAsync(keywords, 10);
// Assert
// 即使部分关键字失败,也应该返回成功的结果
icons.Should().NotBeNull();
}
#endregion
}