tests: seed the throttle counters on the clock the routes actually read
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 19s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 19s
The three route tests stamped their pre-loaded hits at t=0..9 through the injected clock, then called a route that reads `time.monotonic()`. Against a trailing window those hits are fifteen minutes stale on arrival, so they were pruned before they could refuse anything, the request carried on to the database that this suite doesn't have, and the assertion read `500 == 429`. The window's own tests keep the injected clock — they pass the same one to both sides, which is what makes them deterministic and instant. Only the tests that hand off to a route need the real one.
This commit is contained in:
+15
-6
@@ -8,6 +8,8 @@ request returns 429 before any session is opened, which is the whole point of
|
|||||||
checking the limit before the password. The happy path can't be reached here and is
|
checking the limit before the password. The happy path can't be reached here and is
|
||||||
not pretended at.
|
not pretended at.
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync import ratelimit
|
from thoughtsync import ratelimit
|
||||||
@@ -90,8 +92,13 @@ async def test_login_starts_refusing(app):
|
|||||||
body = {"email": "someone@example.com", "password": "wrong-password"}
|
body = {"email": "someone@example.com", "password": "wrong-password"}
|
||||||
# Pre-load the account's counter to its limit rather than posting that many
|
# Pre-load the account's counter to its limit rather than posting that many
|
||||||
# times: every real attempt would need a database to reach the password check.
|
# times: every real attempt would need a database to reach the password check.
|
||||||
for i in range(ratelimit.ACCOUNT_LIMIT):
|
#
|
||||||
ratelimit.sign_in_by_account.record("someone@example.com", now=float(i))
|
# On the REAL clock, not an injected one. The window is trailing, so hits stamped
|
||||||
|
# at t=0..9 are fifteen minutes stale the moment the route reads
|
||||||
|
# `time.monotonic()` and get pruned before they can refuse anything.
|
||||||
|
now = time.monotonic()
|
||||||
|
for _ in range(ratelimit.ACCOUNT_LIMIT):
|
||||||
|
ratelimit.sign_in_by_account.record("someone@example.com", now=now)
|
||||||
resp = await client.post("/api/auth/login", json=body)
|
resp = await client.post("/api/auth/login", json=body)
|
||||||
assert resp.status_code == 429
|
assert resp.status_code == 429
|
||||||
assert resp.headers.get("Retry-After")
|
assert resp.headers.get("Retry-After")
|
||||||
@@ -101,8 +108,9 @@ async def test_login_starts_refusing(app):
|
|||||||
|
|
||||||
async def test_device_login_shares_the_account_counter(app):
|
async def test_device_login_shares_the_account_counter(app):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
for i in range(ratelimit.ACCOUNT_LIMIT):
|
now = time.monotonic()
|
||||||
ratelimit.sign_in_by_account.record("someone@example.com", now=float(i))
|
for _ in range(ratelimit.ACCOUNT_LIMIT):
|
||||||
|
ratelimit.sign_in_by_account.record("someone@example.com", now=now)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/auth/device-login",
|
"/api/auth/device-login",
|
||||||
json={"email": "someone@example.com", "password": "wrong-password"},
|
json={"email": "someone@example.com", "password": "wrong-password"},
|
||||||
@@ -114,8 +122,9 @@ async def test_device_login_shares_the_account_counter(app):
|
|||||||
|
|
||||||
async def test_register_is_throttled_by_address(app):
|
async def test_register_is_throttled_by_address(app):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
for i in range(ratelimit.REGISTER_LIMIT):
|
now = time.monotonic()
|
||||||
ratelimit.register_by_address.record("203.0.113.9", now=float(i))
|
for _ in range(ratelimit.REGISTER_LIMIT):
|
||||||
|
ratelimit.register_by_address.record("203.0.113.9", now=now)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
json={"email": "new@example.com", "password": "a-long-enough-password"},
|
json={"email": "new@example.com", "password": "a-long-enough-password"},
|
||||||
|
|||||||
Reference in New Issue
Block a user