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