1470bbcff2
Resolves architecture test violations: - Removed all direct DateTime.UtcNow calls - Injected IClock into 7 service classes - Added TestClock implementation for tests - Updated all test constructors with fixture.Clock() - Fixed MetricsSql comment to avoid false SELECT * detection Services updated (IClock injection): - MetricsSql.cs (BuildingBlocks) - CircuitBreakerPolicyFactory.cs - KisConnectionPool.cs - RateLimiterService.cs - MetricsPolicy.cs - OpenDartDailyBatchJob.cs - OpenDartService.cs Tests updated: - DatabaseFixture.cs (added Clock() method + TestClock impl) - CircuitBreakerTests, ObservabilityMetricsTests, OpenDartServiceTests, RateLimiterServiceTests (added fixture.Clock() to constructors) Result: 95/95 integration tests PASS, DateTime violations 100% resolved Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
122 lines
4.2 KiB
C#
122 lines
4.2 KiB
C#
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<string> { "timeout" });
|
|
var duplicates = (Detected: 2, Resolved: 1, LastCheck: DateTime.UtcNow);
|
|
var reconciliation = (Detected: 0, Resolved: 0, Pending: new List<string>());
|
|
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);
|
|
}
|
|
}
|