using Dapper; using KArtSell.Host.Observability; using Npgsql; using Xunit; namespace KArtSell.Integration.Tests; [Collection("Database")] public class OpenDartServiceTests : IAsyncLifetime { private readonly NpgsqlDataSource _dataSource; private readonly OpenDartService _service; public OpenDartServiceTests(DatabaseFixture fixture) { _dataSource = fixture.DataSource; _service = new OpenDartService(_dataSource, new HttpClient(), fixture.Logger()); } public async Task InitializeAsync() { // Create opendata schema if needed await using var conn = await _dataSource.OpenConnectionAsync(); await conn.ExecuteAsync(""" CREATE SCHEMA IF NOT EXISTS opendata; CREATE TABLE IF NOT EXISTS opendata.opendart_cache ( id BIGSERIAL PRIMARY KEY, ticker VARCHAR(10) NOT NULL, quarter VARCHAR(6) NOT NULL, data_json JSONB NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, published_at TIMESTAMP WITH TIME ZONE NOT NULL, UNIQUE(ticker, quarter) ); TRUNCATE opendata.opendart_cache; """); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task GetQuarterlyFinancialData_CachesResult_OnSuccess() { // Arrange var ticker = "005930"; // Samsung var quarter = "2024-Q1"; // Act - First call should cache var result1 = await _service.GetQuarterlyFinancialDataAsync(ticker, quarter); // Assert - Verify cache entry exists await using var conn = await _dataSource.OpenConnectionAsync(); var cached = await conn.QueryFirstOrDefaultAsync( "SELECT data_json FROM opendata.opendart_cache WHERE ticker = @ticker AND quarter = @quarter", new { ticker, quarter }); Assert.NotNull(cached); } [Fact] public async Task GetQuarterlyFinancialData_ReturnsFromCache_OnSecondCall() { // Arrange var ticker = "005930"; var quarter = "2024-Q1"; // Manually insert cache entry await using var conn = await _dataSource.OpenConnectionAsync(); await conn.ExecuteAsync(""" INSERT INTO opendata.opendart_cache (ticker, quarter, data_json, expires_at, published_at) VALUES (@ticker, @quarter, @data, @expires, @now) ON CONFLICT DO NOTHING """, new { ticker, quarter, data = """{"ticker":"005930","revenue":100000}""", expires = DateTime.UtcNow.AddDays(1), now = DateTime.UtcNow }); // Act - Should return cached without API call var result = await _service.GetQuarterlyFinancialDataAsync(ticker, quarter); // Assert Assert.NotNull(result); Assert.Equal("005930", result.Ticker); } [Fact] public async Task GetQuarterlyFinancialData_Idempotent_MultipleCalls() { // Arrange var ticker = "005930"; var quarter = "2024-Q1"; // Act - Multiple calls should return same cached result var result1 = await _service.GetQuarterlyFinancialDataAsync(ticker, quarter); var result2 = await _service.GetQuarterlyFinancialDataAsync(ticker, quarter); // Assert - No API calls triggered (idempotent) await using var conn = await _dataSource.OpenConnectionAsync(); var cacheCount = await conn.QueryFirstOrDefaultAsync( "SELECT COUNT(*) FROM opendata.opendart_cache WHERE ticker = @ticker AND quarter = @quarter", new { ticker, quarter }); Assert.Equal(1, cacheCount); // Only 1 cache entry despite 2 calls } }