using Dapper; using KArtSell.BuildingBlocks.Observability; using KArtSell.Host.Features.Observability; using Npgsql; using Xunit; namespace KArtSell.Integration.Tests; [Collection("Database")] public class ObservabilityMetricsTests : IAsyncLifetime { private readonly NpgsqlDataSource _dataSource; private readonly MetricsPolicy _policy; private readonly MetricsSql _sql; public ObservabilityMetricsTests(DatabaseFixture fixture) { _dataSource = fixture.DataSource; _policy = new MetricsPolicy(fixture.Clock()); _sql = new MetricsSql(_dataSource, fixture.Clock()); } public async Task InitializeAsync() { await using var conn = await _dataSource.OpenConnectionAsync(); await conn.ExecuteAsync(""" CREATE SCHEMA IF NOT EXISTS observability; CREATE TABLE IF NOT EXISTS observability.batch_sla_metrics ( id BIGSERIAL PRIMARY KEY, job_name VARCHAR(100) NOT NULL, job_type VARCHAR(50) NOT NULL, started_at TIMESTAMP WITH TIME ZONE NOT NULL, completed_at TIMESTAMP WITH TIME ZONE NOT NULL, duration_seconds INT NOT NULL, status VARCHAR(50) NOT NULL, recorded_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS observability.data_quality_quarantine ( id BIGSERIAL PRIMARY KEY, module_name VARCHAR(100) NOT NULL, reason VARCHAR(256) NOT NULL, entity_id UUID, entity_type VARCHAR(50), quarantined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, resolution_status VARCHAR(50), published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); TRUNCATE observability.batch_sla_metrics CASCADE; TRUNCATE observability.data_quality_quarantine CASCADE; """); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public void BuildMetricsResponse_ReturnsValidSchema() { // Arrange var batchSla = (Total: 10, OnTime: 8, AvgTime: new TimeSpan(0, 5, 30)); var dataQuality = (Quarantined: 1, Total: 100, Errors: new List { "timeout" }); var duplicates = (Detected: 2, Resolved: 1, LastCheck: DateTime.UtcNow); var reconciliation = (Detected: 0, Resolved: 0, Pending: new List()); var modelDrift = (Baseline: 1.5m, Current: 1.0m); // 33% drift (WARNING threshold is 15%, CRITICAL is 30%) // Act var response = _policy.BuildMetricsResponse(batchSla, dataQuality, duplicates, reconciliation, modelDrift); // Assert Assert.NotNull(response); Assert.Equal(80, response.BatchSla.SlaPercentage); Assert.Equal(99, response.DataQuality.QualityPercentage); Assert.Equal("CRITICAL", response.ModelDrift.Status); // 33% drift } [Fact] public void BuildBatchSlaMetrics_CalculatesPercentageCorrectly() { // Arrange var batchSla = (Total: 100, OnTime: 95, AvgTime: new TimeSpan(0, 10, 0)); // Act var response = _policy.BuildMetricsResponse(batchSla, null, null, null, null); // Assert Assert.Equal(95m, response.BatchSla.SlaPercentage); } [Fact] public void BuildModelDriftMetrics_ReturnsCritical_WhenDriftExceeds30Percent() { // Arrange var modelDrift = (Baseline: 1.0m, Current: 0.5m); // 50% loss // Act var response = _policy.BuildMetricsResponse(null, null, null, null, modelDrift); // Assert Assert.Equal("CRITICAL", response.ModelDrift.Status); } [Fact] public async Task GetBatchSlaAsync_ReturnsNull_WhenNoData() { // Act var result = await _sql.GetBatchSlaAsync(); // Assert Assert.Null(result); } [Fact] public async Task GetDataQualityQuarantineAsync_ReturnsNull_WhenNoData() { // Act var result = await _sql.GetDataQualityQuarantineAsync(); // Assert Assert.Null(result); } }