P0 - 安全修复: - 移除硬编码 API Key,启动时校验必填环境变量 P1 - 高优先级: - Entity 拆分:Position.cs, Transaction.cs 独立文件 - Controller Facade 封装:IPortfolioFacade 减少依赖注入 P2 - 中优先级: - Repository 抽象:IPortfolioRepository, IMarketDataRepository - MarketDataService 拆分:组合模式整合 Tencent/Tiingo/OKX P3 - 低优先级: - DTO 命名规范:统一 PascalCase - 单元测试框架:xUnit + Moq + FluentAssertions
185 lines
5.3 KiB
C#
185 lines
5.3 KiB
C#
using AssetManager.Data.Repositories;
|
|
using AssetManager.Infrastructure.Services;
|
|
using AssetManager.Models.DTOs;
|
|
using AssetManager.Services;
|
|
using FluentAssertions;
|
|
using Moq;
|
|
using SqlSugar;
|
|
using Xunit;
|
|
|
|
namespace AssetManager.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// PortfolioService 单元测试
|
|
/// </summary>
|
|
public class PortfolioServiceTests
|
|
{
|
|
private readonly Mock<ISqlSugarClient> _dbMock;
|
|
private readonly Mock<IMarketDataService> _marketDataServiceMock;
|
|
private readonly Mock<IExchangeRateService> _exchangeRateServiceMock;
|
|
private readonly Mock<IPortfolioNavService> _navServiceMock;
|
|
private readonly Mock<ILogger<PortfolioService>> _loggerMock;
|
|
private readonly PortfolioService _service;
|
|
|
|
public PortfolioServiceTests()
|
|
{
|
|
_dbMock = new Mock<ISqlSugarClient>();
|
|
_marketDataServiceMock = new Mock<IMarketDataService>();
|
|
_exchangeRateServiceMock = new Mock<IExchangeRateService>();
|
|
_navServiceMock = new Mock<IPortfolioNavService>();
|
|
_loggerMock = new Mock<ILogger<PortfolioService>>();
|
|
|
|
// Note: 实际测试需要更复杂的 SqlSugar mock 设置
|
|
// 这里展示测试结构和模式
|
|
}
|
|
|
|
#region CreatePortfolio Tests
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public void CreatePortfolio_WithValidRequest_ShouldReturnResponse()
|
|
{
|
|
// Arrange
|
|
var request = new CreatePortfolioRequest
|
|
{
|
|
Name = "测试组合",
|
|
Currency = "CNY",
|
|
Stocks = new List<StockItem>
|
|
{
|
|
new() { Code = "AAPL", Name = "Apple", Price = 150, Amount = 10 }
|
|
}
|
|
};
|
|
var userId = "user-001";
|
|
|
|
// Act
|
|
// var response = _service.CreatePortfolio(request, userId);
|
|
|
|
// Assert
|
|
// response.Should().NotBeNull();
|
|
// response.Id.Should().StartWith("port-");
|
|
// response.Currency.Should().Be("CNY");
|
|
}
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public void CreatePortfolio_WithStrategy_ShouldLoadStrategyStocks()
|
|
{
|
|
// Arrange
|
|
var request = new CreatePortfolioRequest
|
|
{
|
|
Name = "风险平价组合",
|
|
Currency = "USD",
|
|
StrategyId = "strategy-001"
|
|
};
|
|
var userId = "user-001";
|
|
|
|
// Act & Assert - 验证策略配置加载逻辑
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetPortfolios Tests
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public void GetPortfolios_ShouldReturnUserPortfolios()
|
|
{
|
|
// Arrange
|
|
var userId = "user-001";
|
|
|
|
// Act
|
|
// var result = _service.GetPortfolios(userId);
|
|
|
|
// Assert
|
|
// result.Should().NotBeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetTotalAssets Tests
|
|
|
|
[Fact(Skip = "需要 Repository mock 设置")]
|
|
public async Task GetTotalAssetsAsync_ShouldConvertToTargetCurrency()
|
|
{
|
|
// Arrange
|
|
var userId = "user-001";
|
|
|
|
_marketDataServiceMock
|
|
.Setup(x => x.GetPriceAsync(It.IsAny<string>(), It.IsAny<string>()))
|
|
.ReturnsAsync(new MarketPriceResponse { Price = 100, PreviousClose = 98 });
|
|
|
|
_exchangeRateServiceMock
|
|
.Setup(x => x.ConvertAmountAsync(It.IsAny<decimal>(), It.IsAny<string>(), It.IsAny<string>()))
|
|
.ReturnsAsync((decimal amount, string _, string _) => amount * 7.2m);
|
|
|
|
// Act
|
|
// var result = await _service.GetTotalAssetsAsync(userId);
|
|
|
|
// Assert
|
|
// result.Should().NotBeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CreateTransaction Tests
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public async Task CreateTransaction_BuyNewStock_ShouldCreatePosition()
|
|
{
|
|
// Arrange
|
|
var request = new CreateTransactionRequest
|
|
{
|
|
PortfolioId = "port-001",
|
|
Type = "buy",
|
|
StockCode = "AAPL",
|
|
Amount = 10,
|
|
Price = 150,
|
|
Currency = "USD"
|
|
};
|
|
var userId = "user-001";
|
|
|
|
// Act
|
|
// var result = await _service.CreateTransaction(request, userId);
|
|
|
|
// Assert
|
|
// result.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public async Task CreateTransaction_SellMoreThanHeld_ShouldThrowException()
|
|
{
|
|
// Arrange
|
|
var request = new CreateTransactionRequest
|
|
{
|
|
PortfolioId = "port-001",
|
|
Type = "sell",
|
|
StockCode = "AAPL",
|
|
Amount = 100, // 超过持仓
|
|
Price = 150,
|
|
Currency = "USD"
|
|
};
|
|
var userId = "user-001";
|
|
|
|
// Act & Assert
|
|
// await Assert.ThrowsAsync<Exception>(() => _service.CreateTransaction(request, userId));
|
|
}
|
|
|
|
[Fact(Skip = "需要 SqlSugar 完整 mock 设置")]
|
|
public async Task CreateTransaction_WrongCurrency_ShouldThrowException()
|
|
{
|
|
// Arrange
|
|
var request = new CreateTransactionRequest
|
|
{
|
|
PortfolioId = "port-001", // 组合币种为 CNY
|
|
Type = "buy",
|
|
StockCode = "AAPL",
|
|
Amount = 10,
|
|
Price = 150,
|
|
Currency = "USD" // 币种不匹配
|
|
};
|
|
var userId = "user-001";
|
|
|
|
// Act & Assert
|
|
// await Assert.ThrowsAsync<Exception>(() => _service.CreateTransaction(request, userId));
|
|
}
|
|
|
|
#endregion
|
|
}
|