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()); } 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); } }