using Xunit; using KArtSell.Modules.ModelOperations.ShadowRun.Services; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace KArtSell.Integration.Tests; /// /// Tests for KRX data service: caching, retry logic, PIT-safe lookups. /// public sealed class KrxDataServiceTests : IAsyncLifetime { private IMemoryCache _cache = null!; private ILogger _logger = null!; private HttpClient _httpClient = null!; public Task InitializeAsync() { _cache = new MemoryCache(new MemoryCacheOptions()); _logger = new NoOpLogger(); _httpClient = new HttpClient(); return Task.CompletedTask; } public Task DisposeAsync() { _cache?.Dispose(); _httpClient?.Dispose(); return Task.CompletedTask; } [Fact] public async Task GetDailyOhlcvAsync_ReturnsBarsForTickerAndDateRange() { // Arrange var service = new KrxDataService(_httpClient, _cache, _logger); var ticker = "005930"; // Samsung var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2024, 1, 5); // Act var bars = await service.GetDailyOhlcvAsync(ticker, startDate, endDate, CancellationToken.None); // Assert Assert.NotEmpty(bars); Assert.All(bars, bar => { Assert.Equal(ticker, bar.Ticker); Assert.True(bar.Date >= startDate && bar.Date <= endDate); Assert.True(bar.Close > 0); Assert.True(bar.High >= bar.Close); Assert.True(bar.Low <= bar.Close); }); } [Fact] public async Task GetDailyOhlcvAsync_CacheHit_ReturnsCachedData() { // Arrange var service = new KrxDataService(_httpClient, _cache, _logger); var ticker = "005930"; var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2024, 1, 5); // Act: First call var bars1 = await service.GetDailyOhlcvAsync(ticker, startDate, endDate, CancellationToken.None); // Act: Second call (should hit cache) var bars2 = await service.GetDailyOhlcvAsync(ticker, startDate, endDate, CancellationToken.None); // Assert: Same data structure (values equal, not necessarily same reference) Assert.Equal(bars1.Count, bars2.Count); Assert.All(Enumerable.Range(0, bars1.Count), i => { Assert.Equal(bars1[i].Ticker, bars2[i].Ticker); Assert.Equal(bars1[i].Date, bars2[i].Date); Assert.Equal(bars1[i].Close, bars2[i].Close); }); } [Fact] public async Task GetFeeScheduleAsync_ReturnsFeeEntries() { // Arrange var service = new KrxDataService(_httpClient, _cache, _logger); var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2024, 1, 31); // Act var fees = await service.GetFeeScheduleAsync(startDate, endDate, CancellationToken.None); // Assert Assert.NotEmpty(fees); Assert.All(fees, fee => { Assert.True(fee.TransactionFeePercent > 0); Assert.True(fee.SlippagePercent > 0); }); } private sealed class NoOpLogger : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => false; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { } } }