diff --git a/src/Constants.cs b/src/Constants.cs index 5739236..e16fea0 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -14,8 +14,9 @@ internal static class Constants // Messages public const string ApiResponseLimitExceeded = "higher API call volume"; + public const string ApiResponseRateLimit = "rate limit"; public const string ApiResponseApiKeyInvalid = "apikey is invalid"; - public const string ApiResponsePremiumEndpoint = "premium endpoint"; + public const string ApiResponsePremiumEndpoint = "This is a premium endpoint"; #region Yahoo Finance diff --git a/src/Services/AlphaVantageService.cs b/src/Services/AlphaVantageService.cs index 57eee20..9794ab4 100644 --- a/src/Services/AlphaVantageService.cs +++ b/src/Services/AlphaVantageService.cs @@ -81,7 +81,7 @@ public AlphaVantageService( var jsonResponse = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); ThrowIfRejected(jsonResponse); - if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded)) + if (IsRateLimited(jsonResponse)) { throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}"); } @@ -128,7 +128,7 @@ public async Task> GetRecordsAsync(string symbol, DateTime? { var jsonResponse = await Helper.FetchJsonDocumentAsync(httpClient, _logger, url, ct).ConfigureAwait(false); ThrowIfRejected(jsonResponse); - if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded)) + if (IsRateLimited(jsonResponse)) { throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}"); } @@ -200,7 +200,7 @@ private async Task> GetIntradayRecordsByMonthAsync(string s var jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); ThrowIfRejected(jsonResponse); - if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded)) + if (IsRateLimited(jsonResponse)) { throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}"); } @@ -244,7 +244,7 @@ public async Task> GetForexRecordsAsync(string currency { var jsonResponse = await Helper.FetchJsonDocumentAsync(httpClient, _logger, url, ct).ConfigureAwait(false); ThrowIfRejected(jsonResponse); - if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded)) + if (IsRateLimited(jsonResponse)) { throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {currency1} /{currency2}"); } @@ -262,16 +262,13 @@ public async Task> GetForexRecordsAsync(string currency } } - /// - /// Alpha Vantage answers a refused request with HTTP 200 and a JSON message instead of data. - /// A premium-only endpoint or a bad API key will not change on retry, so fail immediately. - /// + // Alpha Vantage refuses with HTTP 200 and a JSON message; a premium endpoint or a bad key will not change on retry. private static void ThrowIfRejected(string jsonResponse) { if (jsonResponse.Contains(Constants.ApiResponsePremiumEndpoint)) { throw new FinanceNetAccessDeniedException( - $"Alpha Vantage rejected the request: this is a {Constants.ApiResponsePremiumEndpoint} and needs a premium plan"); + "Alpha Vantage rejected the request: this is a premium endpoint and needs a premium plan"); } if (jsonResponse.Contains(Constants.ApiResponseApiKeyInvalid)) { @@ -279,4 +276,7 @@ private static void ThrowIfRejected(string jsonResponse) $"Alpha Vantage rejected the request: {Constants.ApiResponseApiKeyInvalid} or missing"); } } + + private static bool IsRateLimited(string jsonResponse) => + jsonResponse.Contains(Constants.ApiResponseRateLimit) || jsonResponse.Contains(Constants.ApiResponseLimitExceeded); } diff --git a/tests/IntegrationTests/AlphaVantageTests.cs b/tests/IntegrationTests/AlphaVantageTests.cs index 42708b1..29ff3b5 100644 --- a/tests/IntegrationTests/AlphaVantageTests.cs +++ b/tests/IntegrationTests/AlphaVantageTests.cs @@ -40,7 +40,7 @@ public async Task GetOverviewAsync(string symbol, bool shouldHave) } else { - Assert.ThrowsAsync(async () => await _service.GetOverviewAsync(symbol)); + Assert.CatchAsync(async () => await _service.GetOverviewAsync(symbol)); } } @@ -61,7 +61,7 @@ public async Task GetRecordsAsync(string symbol, bool shouldHave) } else { - Assert.ThrowsAsync(async () => await _service.GetRecordsAsync(symbol, DateTime.UtcNow.AddDays(-7))); + Assert.CatchAsync(async () => await _service.GetRecordsAsync(symbol, DateTime.UtcNow.AddDays(-7))); } } @@ -85,7 +85,7 @@ public async Task GetIntradayRecordsAsync(string symbol, EInterval eInterval, bo } else { - Assert.ThrowsAsync(async () => await _service.GetIntradayRecordsAsync(symbol, startDay, endDay, eInterval)); + Assert.CatchAsync(async () => await _service.GetIntradayRecordsAsync(symbol, startDay, endDay, eInterval)); } } @@ -101,7 +101,7 @@ public async Task GetForexRecordsAsync(string currency1, string currency2, bool } else { - Assert.ThrowsAsync(async () => await _service.GetForexRecordsAsync(currency1, currency2, DateTime.UtcNow.AddDays(-3))); + Assert.CatchAsync(async () => await _service.GetForexRecordsAsync(currency1, currency2, DateTime.UtcNow.AddDays(-3))); } } } diff --git a/tests/Services/AlphaVantageRejectedRequestTests.cs b/tests/Services/AlphaVantageRejectedRequestTests.cs index 4a72fba..29c0841 100644 --- a/tests/Services/AlphaVantageRejectedRequestTests.cs +++ b/tests/Services/AlphaVantageRejectedRequestTests.cs @@ -96,18 +96,19 @@ public void InvalidApiKey_FailsFastWithoutRetrying(string name, Func(async () => await call(_service), name); Assert.That(exception.Message, Does.Contain("apikey is invalid"), name); + Assert.That(exception.InnerException, Is.Null, name); Assert.That(_requestCount, Is.EqualTo(1), $"{name}: an invalid-API-key refusal was retried"); } - [Test] - public void ApiLimitExceeded_IsStillRetried() + [TestCaseSource(nameof(Calls))] + public void RateLimit_IsStillRetried(string name, Func call) { - // The per-minute limit clears on its own, so it stays a retryable failure. - SetUpResponse("{\"Information\": \"Please consider spreading out your free API requests more sparingly (1 request per second). You may subscribe to any of the premium plans at https://www.alphavantage.co/premium/ to lift the free key rate limit, or visit higher API call volume.\"}"); + // The rate-limit notice advertises "premium endpoints" too, but clears on its own. + SetUpResponse("{\"Information\": \"Thank you for using Alpha Vantage! Please consider spreading out your free API requests more sparingly (1 request per second). You may subscribe to any of the premium plans at https://www.alphavantage.co/premium/ to lift the free key rate limit (25 requests per day), raise the per-second burst limit, and instantly unlock all premium endpoints\"}"); - var exception = Assert.CatchAsync(async () => await _service.GetRecordsAsync("IBM", StartDate)); - Assert.That(exception, Is.Not.InstanceOf()); - Assert.That(_requestCount, Is.EqualTo(RetryCount + 1)); + var exception = Assert.CatchAsync(async () => await call(_service), name); + Assert.That(exception, Is.Not.InstanceOf(), name); + Assert.That(_requestCount, Is.EqualTo(RetryCount + 1), $"{name}: a rate-limit notice was not retried"); } [Test]