using System.Net;
using FluentAssertions;
using IBKRTrader.Core.Logging;
using IBKRTrader.Modules.CongressTrading.Scraper;
namespace IBKRTrader.Tests.Modules;
///
/// Verhalten des Scrapers, wenn capitoltrades.com den Abruf begrenzt. Ohne Netzwerk: der
/// Nachrichten-Handler wird untergeschoben und zählt die Anfragen mit.
///
[Trait("cat", "unit")]
public class CapitolTradesRateLimitTests
{
/// Antwortet mit einer festen Folge von Antworten und zählt die Anfragen.
private sealed class StubHandler(Func responder) : HttpMessageHandler
{
public int Requests;
protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct)
{
var n = Interlocked.Increment(ref Requests);
return Task.FromResult(responder(n));
}
}
private static HttpResponseMessage TooManyRequests(TimeSpan retryAfter)
{
var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(retryAfter);
return response;
}
[Fact]
public async Task RateLimit_EndsWithRateLimitedException_NotAnHttpError()
{
// Retry-After deutlich über dem, was innerhalb eines Laufs abgewartet wird → sofort zurückstellen.
var handler = new StubHandler(_ => TooManyRequests(TimeSpan.FromHours(1)));
var scraper = new CapitolTradesScraper(new LoggingService(), handler);
var act = async () => await scraper.ScrapeTradesPageAsync(1, CancellationToken.None);
var ex = await act.Should().ThrowAsync();
ex.Which.RetryAfterUtc.Should().BeAfter(DateTime.UtcNow);
handler.Requests.Should().Be(1, "ohne Retry-After-Spielraum wird nicht nachgefasst");
}
[Fact]
public async Task AfterRateLimit_NoFurtherRequestsUntilBlockExpires()
{
var handler = new StubHandler(_ => TooManyRequests(TimeSpan.FromHours(1)));
var scraper = new CapitolTradesScraper(new LoggingService(), handler);
await Assert.ThrowsAsync(
() => scraper.ScrapeTradesPageAsync(1, CancellationToken.None));
// Zweiter Aufruf – etwa der andere CT-Worker – läuft nicht erneut gegen die Sperre.
await Assert.ThrowsAsync(
() => scraper.GetTotalPagesAsync(CancellationToken.None));
handler.Requests.Should().Be(1, "das Sperrfenster hält weitere Abrufe zurück");
}
[Fact]
public async Task OtherHttpErrors_StayErrors()
{
var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError));
var scraper = new CapitolTradesScraper(new LoggingService(), handler);
await Assert.ThrowsAsync(
() => scraper.ScrapeTradesPageAsync(1, CancellationToken.None));
}
[Fact]
public async Task SuccessfulResponse_IsParsedAsBefore()
{
var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(@"self.__next_f.push([1,""\""totalPages\"":7""])")
});
var scraper = new CapitolTradesScraper(new LoggingService(), handler);
var pages = await scraper.GetTotalPagesAsync(CancellationToken.None);
pages.Should().Be(7);
handler.Requests.Should().Be(1);
}
}