From ef6c2ae364a647559e3e878f4266ae10a988a69f Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Sun, 14 Jun 2026 21:12:51 -0700 Subject: [PATCH] Allow zero submission rate limits --- src/kernelbot/api/main.py | 4 +-- .../20260319_01_allow-zero-rate-limits.py | 28 +++++++++++++++++++ tests/test_admin_api.py | 4 +-- tests/test_leaderboard_db.py | 10 +++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/migrations/20260319_01_allow-zero-rate-limits.py diff --git a/src/kernelbot/api/main.py b/src/kernelbot/api/main.py index 3417efa15..764d00627 100644 --- a/src/kernelbot/api/main.py +++ b/src/kernelbot/api/main.py @@ -1131,8 +1131,8 @@ async def admin_set_rate_limit( if mode_category not in ("test", "leaderboard"): raise HTTPException(status_code=400, detail="mode_category must be 'test' or 'leaderboard'") max_per_hour = payload.get("max_submissions_per_hour") - if not isinstance(max_per_hour, int) or max_per_hour < 1: - raise HTTPException(status_code=400, detail="max_submissions_per_hour must be a positive integer") + if not isinstance(max_per_hour, int) or max_per_hour < 0: + raise HTTPException(status_code=400, detail="max_submissions_per_hour must be a non-negative integer") try: with db_context as db: result = db.set_rate_limit(leaderboard_name, mode_category, max_per_hour) diff --git a/src/migrations/20260319_01_allow-zero-rate-limits.py b/src/migrations/20260319_01_allow-zero-rate-limits.py new file mode 100644 index 000000000..bc210761f --- /dev/null +++ b/src/migrations/20260319_01_allow-zero-rate-limits.py @@ -0,0 +1,28 @@ +""" +Allow zero-submission rate limits. +""" + +from yoyo import step + +__depends__ = {"20260318_01_ban-user"} + +steps = [ + step( + """ + ALTER TABLE leaderboard.rate_limit + DROP CONSTRAINT rate_limit_max_submissions_per_hour_check; + + ALTER TABLE leaderboard.rate_limit + ADD CONSTRAINT rate_limit_max_submissions_per_hour_check + CHECK (max_submissions_per_hour >= 0); + """, + """ + ALTER TABLE leaderboard.rate_limit + DROP CONSTRAINT rate_limit_max_submissions_per_hour_check; + + ALTER TABLE leaderboard.rate_limit + ADD CONSTRAINT rate_limit_max_submissions_per_hour_check + CHECK (max_submissions_per_hour > 0); + """, + ) +] diff --git a/tests/test_admin_api.py b/tests/test_admin_api.py index 08b89c1d7..2ad3ab3dd 100644 --- a/tests/test_admin_api.py +++ b/tests/test_admin_api.py @@ -800,11 +800,11 @@ def test_set_rate_limit_invalid_category(self, test_client): assert response.status_code == 400 def test_set_rate_limit_invalid_count(self, test_client): - """PUT /admin/leaderboards/{name}/rate-limits rejects non-positive count.""" + """PUT /admin/leaderboards/{name}/rate-limits rejects negative count.""" response = test_client.put( "/admin/leaderboards/test-lb/rate-limits", headers={"Authorization": "Bearer test_token"}, - json={"mode_category": "test", "max_submissions_per_hour": 0}, + json={"mode_category": "test", "max_submissions_per_hour": -1}, ) assert response.status_code == 400 diff --git a/tests/test_leaderboard_db.py b/tests/test_leaderboard_db.py index 1ebf3b290..77c6f4318 100644 --- a/tests/test_leaderboard_db.py +++ b/tests/test_leaderboard_db.py @@ -1153,6 +1153,16 @@ def test_check_rate_limit_no_config(database, submit_leaderboard): assert result is None +def test_check_rate_limit_zero_limit_blocks_submissions(database, submit_leaderboard): + """A zero rate limit keeps a leaderboard visible while rejecting submissions.""" + with database as db: + db.set_rate_limit("submit-leaderboard", "leaderboard", 0) + result = db.check_rate_limit("submit-leaderboard", "123", "leaderboard") + assert result["allowed"] is False + assert result["current_count"] == 0 + assert result["max_per_hour"] == 0 + + def test_check_rate_limit_under_limit(database, submit_leaderboard): """check_rate_limit allows submissions under the limit.""" with database as db: