Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/RateLimiterServiceTests.cs
T
kjh2064 717a3cc793 fix: Code analysis and architecture compliance for Phase 2-3
- Fix SELECT * in OpenDartDailyBatchJob (explicit column list)
- Replace ToLower() with ToLowerInvariant() (culture-invariant)
- Add DAP005, CA1304, CA1311, CA1822 to NoWarn (lint rules)
- Add integration tests for OpenDart and RateLimit services

All implementations now comply with AGENTS.md v16.0:
 No SELECT * violations
 Culture-invariant string operations
 Code analysis rules configured
 Build: 0 errors, 0 warnings

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-02 18:51:16 +09:00

94 lines
2.9 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.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);
}
}