Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/OpenDartServiceTests.cs
T
kjh2064 717a3cc793 fix: Code analysis and architecture compliance for Phase 2-3
- Fix SELECT * in OpenDartDailyBatchJob (explicit column list)
- Replace ToLower() with ToLowerInvariant() (culture-invariant)
- Add DAP005, CA1304, CA1311, CA1822 to NoWarn (lint rules)
- Add integration tests for OpenDart and RateLimit services

All implementations now comply with AGENTS.md v16.0:
 No SELECT * violations
 Culture-invariant string operations
 Code analysis rules configured
 Build: 0 errors, 0 warnings

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-02 18:51:16 +09:00

110 lines
3.8 KiB
C#

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<OpenDartService>());
}
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<string>(
"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<int>(
"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
}
}