using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using KArtSell.Modules.ModelOperations.Domain; using KArtSell.Host.Features.MarketData; namespace KArtSell.Integration.Tests.Features.MarketData; /// /// VS-03 TESTOPS: Market Data Ingestion Tests (No DB Required) /// /// Validates: /// - Validation logic (Policy) /// - Duplicate detection /// - Data normalization /// - Batch metrics /// - Quality scoring /// public sealed class MarketDataIngestionIntegrationTests { [Fact] public void Policy_ValidatePrice_WithValidData_Returns_Valid() { // Arrange 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()); // Act var result = MarketDataPolicy.ValidatePrice(price); // Assert Assert.True(result.IsValid); Assert.Empty(result.Errors); Assert.True(result.QualityScore >= 90); } [Fact] public void Policy_ValidatePrice_WithNegativePrice_Returns_Invalid() { // Arrange 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()); // Act var result = MarketDataPolicy.ValidatePrice(price); // Assert Assert.False(result.IsValid); Assert.NotEmpty(result.Errors); } [Fact] public void Policy_ValidatePrice_WithHighLowViolation_Returns_Invalid() { // Arrange var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 90m, // High < Low 110m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); // Act var result = MarketDataPolicy.ValidatePrice(price); // Assert Assert.False(result.IsValid); Assert.Contains("High must be >= Low", result.Errors); } [Fact] public void Policy_ValidatePrice_WithZeroVolume_Reduces_QualityScore() { // Arrange var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 0, // Zero volume DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); // Act var result = MarketDataPolicy.ValidatePrice(price); // Assert Assert.True(result.IsValid); Assert.True(result.QualityScore < 80); // Quality reduced due to zero volume } [Fact] public void Policy_ValidatePrice_WithFutureDate_Returns_Invalid() { // Arrange var futureDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)); var price = new DailyPrice( Guid.NewGuid(), "AAPL", futureDate, 100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); // Act var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); // Assert Assert.False(result.IsValid); Assert.Contains("Trading date cannot be in the future", result.Errors); } [Fact] public void Policy_IsDuplicate_WithIdenticalPrice_Returns_True() { // Arrange 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 }; // Act var isDuplicate = MarketDataPolicy.IsDuplicate(price, existing); // Assert Assert.True(isDuplicate); } [Fact] public void Policy_IsDuplicate_WithDifferentSymbol_Returns_False() { // Arrange var price1 = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var price2 = new DailyPrice( Guid.NewGuid(), "MSFT", // Different symbol DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); var existing = new List { price1 }; // Act var isDuplicate = MarketDataPolicy.IsDuplicate(price2, existing); // Assert Assert.False(isDuplicate); } [Fact] public void Policy_NormalizePrice_WithValidVolume_Returns_Normalized() { // Arrange var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100.123m, 110.456m, 90.789m, 105.012m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); // Act var normalized = MarketDataPolicy.NormalizePrice(price); // Assert Assert.NotNull(normalized); Assert.Equal(100.12m, normalized!.OpenPrice); // Rounded to 2 decimals Assert.Equal(110.46m, normalized.HighPrice); Assert.Equal(90.79m, normalized.LowPrice); Assert.Equal(105.01m, normalized.ClosePrice); } [Fact] public void Policy_NormalizePrice_WithLowVolume_Returns_Null() { // Arrange var price = new DailyPrice( Guid.NewGuid(), "AAPL", DateOnly.FromDateTime(DateTime.UtcNow), 100m, 110m, 90m, 105m, 50, // Low volume DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()); // Act var normalized = MarketDataPolicy.NormalizePrice(price); // Assert Assert.Null(normalized); } [Fact] public void Policy_ValidateBatch_Returns_Aggregated_Metrics() { // Arrange 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(), "GOOGL", DateOnly.FromDateTime(DateTime.UtcNow), 200m, 190m, 210m, 205m, 1_000_000, DateTime.UtcNow, 1, "KRX", Guid.NewGuid().ToString()), }, new(), Guid.NewGuid().ToString()); // Act var (total, valid, invalid, quality) = MarketDataPolicy.ValidateBatch(batch); // Assert Assert.Equal(3, total); Assert.Equal(1, valid); // Only AAPL is valid Assert.Equal(2, invalid); // MSFT (negative), GOOGL (high= 0 && quality <= 100); } [Fact] public void Policy_ClassifyQualityIssue_HighScore_Returns_Accept() { // Arrange var result = new ValidationResult(true, new(), 95); // Act var decision = MarketDataPolicy.ClassifyQualityIssue(result); // Assert Assert.Equal(DataQualityDecision.Accept, decision); } [Fact] public void Policy_ClassifyQualityIssue_MediumScore_Returns_AcceptWithWarning() { // Arrange var result = new ValidationResult(true, new(), 75); // Act var decision = MarketDataPolicy.ClassifyQualityIssue(result); // Assert Assert.Equal(DataQualityDecision.AcceptWithWarning, decision); } [Fact] public void Policy_ClassifyQualityIssue_LowScore_Returns_Quarantine() { // Arrange var result = new ValidationResult(true, new(), 55); // Act var decision = MarketDataPolicy.ClassifyQualityIssue(result); // Assert Assert.Equal(DataQualityDecision.Quarantine, decision); } [Fact] public void Policy_ClassifyQualityIssue_VeryLowScore_Returns_Reject() { // Arrange var result = new ValidationResult(false, new() { "Multiple errors" }, 25); // Act var decision = MarketDataPolicy.ClassifyQualityIssue(result); // Assert Assert.Equal(DataQualityDecision.Reject, decision); } }