using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using KArtSell.Modules.ModelOperations.Domain; namespace KArtSell.Integration.Tests.Features.MarketData; /// /// VS-03 TESTOPS: Market Data Ingestion Tests /// /// Split into: /// - Unit tests (policy logic, no I/O) — run always /// - Integration tests (DB-backed) — skipped if SSH tunnel unavailable /// /// AGENTS.md v16.0 compliance: Graceful skip vs deletion /// public sealed class MarketDataIngestionUnitTests { [Fact] public void Policy_ValidatePrice_WithValidData_Returns_Valid() { var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 102m, 99m, 101m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.True(result.IsValid); Assert.Empty(result.Errors); } [Fact] public void Policy_ValidatePrice_WithNegativePrice_Returns_Invalid() { var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), -100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.False(result.IsValid); Assert.NotEmpty(result.Errors); } [Fact] public void Policy_IsDuplicate_WithIdenticalPrice_Returns_True() { var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var existing = new List { price }; var isDuplicate = MarketDataPolicy.IsDuplicate(price, existing); Assert.True(isDuplicate); } [Fact] public void Policy_NormalizePrice_WithLowVolume_Returns_Null() { var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 50, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var normalized = MarketDataPolicy.NormalizePrice(price); Assert.Null(normalized); } [Fact] public void Policy_ValidateBatch_Returns_Aggregated_Metrics() { var batch = new IngestionBatch( Guid.NewGuid(), "KRX", DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), DateOnly.FromDateTime(DateTime.UtcNow), new List { new(Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()), new(Guid.NewGuid(), "MSFT", DateOnly.FromDateTime(DateTime.UtcNow), -50m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()), }, new(), Guid.NewGuid().ToString()); var (total, valid, invalid, quality) = MarketDataPolicy.ValidateBatch(batch); Assert.Equal(2, total); Assert.Equal(1, valid); Assert.Equal(1, invalid); } [Fact] public void Policy_ClassifyQualityIssue_HighScore_Returns_Accept() { var result = new ValidationResult(true, new(), 95); var decision = MarketDataPolicy.ClassifyQualityIssue(result); Assert.Equal(DataQualityDecision.Accept, decision); } [Theory] [InlineData(75, DataQualityDecision.AcceptWithWarning)] [InlineData(55, DataQualityDecision.Quarantine)] [InlineData(25, DataQualityDecision.Reject)] public void Policy_ClassifyQualityIssue_MapsScoresToDecisions(int score, DataQualityDecision expected) { var result = new ValidationResult(true, new(), score); var decision = MarketDataPolicy.ClassifyQualityIssue(result); Assert.Equal(expected, decision); } } /// /// DB-backed integration tests (SKIPPED - require SSH tunnel + active PostgreSQL) /// Marked with [Fact(Skip = "...")] so they appear in test results as deferred, not deleted /// AGENTS.md v16.0: Failing/skipped tests must be marked, not deleted silently /// public sealed class MarketDataIngestionIntegrationTests { [Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")] public async Task Integration_PersistPrice_To_Database() { // Placeholder: requires SSH tunnel to 178.104.200.7:5432 // Execute: ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 before running await Task.CompletedTask; } [Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")] public async Task Integration_ScheduleIngestion_Creates_Job_Record() { await Task.CompletedTask; } [Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")] public async Task Integration_Idempotency_No_ReRun_For_Same_DateRange() { await Task.CompletedTask; } [Fact(Skip = "DB integration test — skipped (SSH tunnel required, see CLAUDE.md)")] public async Task Integration_EventPublishing_Inserts_To_Outbox() { await Task.CompletedTask; } }