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>
94 lines
3.0 KiB
C#
94 lines
3.0 KiB
C#
using Dapper;
|
|
using KArtSell.Host.Infrastructure;
|
|
using Npgsql;
|
|
using Xunit;
|
|
|
|
namespace KArtSell.Integration.Tests;
|
|
|
|
[Collection("Database")]
|
|
public class RateLimiterServiceTests : IAsyncLifetime
|
|
{
|
|
private readonly NpgsqlDataSource _dataSource;
|
|
private readonly RateLimiterService _service;
|
|
|
|
public RateLimiterServiceTests(DatabaseFixture fixture)
|
|
{
|
|
_dataSource = fixture.DataSource;
|
|
_service = new RateLimiterService(_dataSource, fixture.Clock(), fixture.Logger<RateLimiterService>());
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await using var conn = await _dataSource.OpenConnectionAsync();
|
|
await conn.ExecuteAsync("""
|
|
CREATE SCHEMA IF NOT EXISTS infrastructure;
|
|
CREATE TABLE IF NOT EXISTS infrastructure.rate_limit_quota (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
api_name VARCHAR(50) NOT NULL UNIQUE,
|
|
limit_count INT NOT NULL,
|
|
window_seconds INT NOT NULL,
|
|
current_tokens DECIMAL NOT NULL,
|
|
last_reset_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
published_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS infrastructure.rate_limit_events (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
api_name VARCHAR(50) NOT NULL,
|
|
action VARCHAR(50) NOT NULL,
|
|
executed_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
published_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
);
|
|
TRUNCATE infrastructure.rate_limit_quota CASCADE;
|
|
""");
|
|
|
|
await _service.InitializeAsync();
|
|
}
|
|
|
|
public Task DisposeAsync() => Task.CompletedTask;
|
|
|
|
[Fact]
|
|
public async Task TryConsumeAsync_ReturnsTrue_WhenTokensAvailable()
|
|
{
|
|
// Act
|
|
var (success, _) = await _service.TryConsumeAsync("krx");
|
|
|
|
// Assert
|
|
Assert.True(success);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryConsumeAsync_ExhaustsQuota_AfterLimitReached()
|
|
{
|
|
// Arrange - KRX limit is 100/min
|
|
// Act - Consume all tokens
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
var (success, _) = await _service.TryConsumeAsync("krx");
|
|
Assert.True(success);
|
|
}
|
|
|
|
// Act - 101st attempt should fail
|
|
var (finalSuccess, retryAfter) = await _service.TryConsumeAsync("krx");
|
|
|
|
// Assert
|
|
Assert.False(finalSuccess);
|
|
Assert.Equal(60, retryAfter); // Window is 60 seconds
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetQuotaAsync_Idempotent_RestoresTokens()
|
|
{
|
|
// Arrange - Consume some tokens
|
|
for (int i = 0; i < 50; i++)
|
|
await _service.TryConsumeAsync("opendart");
|
|
|
|
// Act - Reset quota
|
|
await _service.ResetQuotaAsync("opendart");
|
|
|
|
// Assert - Tokens restored
|
|
var (success, _) = await _service.TryConsumeAsync("opendart");
|
|
Assert.True(success);
|
|
}
|
|
}
|