mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-08-11 11:35:44 -05:00
152 lines
4.2 KiB
C++
152 lines
4.2 KiB
C++
#include "gtest/gtest.h"
|
|
#include <QDateTime>
|
|
#include <QTimeZone>
|
|
#include <libcockatrice/utility/server_rate_limiter.h>
|
|
|
|
namespace
|
|
{
|
|
|
|
const QString HOST = "api.scryfall.com";
|
|
|
|
QDateTime timeAt(qint64 secsFromEpoch)
|
|
{
|
|
return QDateTime::fromMSecsSinceEpoch(secsFromEpoch * 1000, QTimeZone::UTC);
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, First429SetsThirtySecondBackoff)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
QDateTime deadline = limiter.on429(HOST, now);
|
|
|
|
EXPECT_TRUE(limiter.isRateLimited(HOST, now));
|
|
EXPECT_EQ(now.addSecs(30), deadline);
|
|
EXPECT_EQ(1, limiter.rounds(HOST));
|
|
EXPECT_FALSE(limiter.isRateLimited(HOST, deadline));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, RetryAfterHeaderIsHonoredWhenLarger)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
QDateTime deadline = limiter.on429(HOST, now, 45 * 1000);
|
|
|
|
EXPECT_EQ(now.addSecs(45), deadline);
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, RetryAfterSmallerThanFloorIsIgnored)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
QDateTime deadline = limiter.on429(HOST, now, 5 * 1000);
|
|
|
|
EXPECT_EQ(now.addSecs(30), deadline);
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, Concurrent429sDoNotEscalateOrShorten)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
QDateTime first = limiter.on429(HOST, now);
|
|
limiter.on429(HOST, now.addMSecs(100));
|
|
limiter.on429(HOST, now.addMSecs(200));
|
|
|
|
// The burst counts as a single round, but each 429 slides the deadline forward.
|
|
EXPECT_EQ(1, limiter.rounds(HOST));
|
|
EXPECT_EQ(now.addMSecs(30200), limiter.deadline(HOST));
|
|
EXPECT_TRUE(limiter.deadline(HOST) >= first);
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, EscalatesAfterWindowPasses)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
limiter.on429(HOST, now);
|
|
limiter.on429(HOST, now.addSecs(31));
|
|
|
|
EXPECT_EQ(2, limiter.rounds(HOST));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, BudgetExhaustionAppliesCooldown)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
QDateTime deadline;
|
|
for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) {
|
|
deadline = limiter.on429(HOST, now.addSecs(31 * i));
|
|
}
|
|
|
|
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
|
EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 60), deadline);
|
|
|
|
// A 429 inside the exhausted cooldown does not escalate, but keeps sliding the cooldown forward.
|
|
limiter.on429(HOST, now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30));
|
|
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
|
EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30 + 60), limiter.deadline(HOST));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, SuccessClearsPenalty)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
limiter.on429(HOST, now);
|
|
limiter.onSuccess(HOST);
|
|
|
|
EXPECT_EQ(0, limiter.rounds(HOST));
|
|
EXPECT_FALSE(limiter.isRateLimited(HOST, now));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, ClearExpiredRefreshesStaleBudget)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
limiter.on429(HOST, now);
|
|
limiter.clearExpired(now.addSecs(400));
|
|
|
|
EXPECT_EQ(0, limiter.rounds(HOST));
|
|
EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(400)));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, ClearExpiredKeepsRecentBudget)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) {
|
|
limiter.on429(HOST, now.addSecs(31 * i));
|
|
}
|
|
limiter.clearExpired(now.addSecs(200));
|
|
|
|
EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST));
|
|
EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(200)));
|
|
}
|
|
|
|
TEST(ServerRateLimiterTest, EarliestDeadlineAcrossHosts)
|
|
{
|
|
ServerRateLimiter limiter;
|
|
QDateTime now = timeAt(1000);
|
|
|
|
limiter.on429("api.scryfall.com", now, 40 * 1000);
|
|
limiter.on429("gatherer.wizards.com", now.addSecs(5));
|
|
|
|
EXPECT_EQ(now.addSecs(35), limiter.earliestDeadline(now));
|
|
EXPECT_EQ(now.addSecs(40), limiter.deadline("api.scryfall.com"));
|
|
EXPECT_EQ(now.addSecs(40), limiter.earliestDeadline(now.addSecs(36)));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|