kin: BATON-SEC-003-backend_dev
This commit is contained in:
parent
097b7af949
commit
f17ee79edb
13 changed files with 593 additions and 125 deletions
|
|
@ -7,6 +7,9 @@ Tests for BATON-SEC-002:
|
|||
UUID notes: RegisterRequest.uuid and SignalRequest.user_id both require a valid
|
||||
UUID v4 pattern (^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$).
|
||||
All constants below satisfy this constraint.
|
||||
|
||||
BATON-SEC-003: POST /api/signal now requires Authorization: Bearer <api_key>.
|
||||
_register_and_get_key() helper returns the api_key from the registration response.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -40,6 +43,13 @@ _UUID_IP_B = "a0000006-0000-4000-8000-000000000006" # per-IP isolation, user
|
|||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _register_and_get_key(client, uuid: str, name: str) -> str:
|
||||
"""Register user and return api_key."""
|
||||
r = await client.post("/api/register", json={"uuid": uuid, "name": name})
|
||||
assert r.status_code == 200
|
||||
return r.json()["api_key"]
|
||||
|
||||
|
||||
def _make_request(headers: dict | None = None, client_host: str = "127.0.0.1") -> Request:
|
||||
"""Build a minimal Starlette Request with given headers and remote address."""
|
||||
scope = {
|
||||
|
|
@ -120,10 +130,10 @@ def test_get_client_ip_returns_unknown_when_no_client_and_no_headers():
|
|||
async def test_signal_rate_limit_returns_429_after_10_requests():
|
||||
"""POST /api/signal returns 429 on the 11th request from the same IP."""
|
||||
async with make_app_client() as client:
|
||||
await client.post("/api/register", json={"uuid": _UUID_SIG_RL, "name": "RL"})
|
||||
api_key = await _register_and_get_key(client, _UUID_SIG_RL, "RL")
|
||||
|
||||
payload = {"user_id": _UUID_SIG_RL, "timestamp": 1742478000000}
|
||||
ip_hdrs = {"X-Real-IP": "5.5.5.5"}
|
||||
ip_hdrs = {"X-Real-IP": "5.5.5.5", "Authorization": f"Bearer {api_key}"}
|
||||
|
||||
statuses = []
|
||||
for _ in range(11):
|
||||
|
|
@ -137,10 +147,10 @@ async def test_signal_rate_limit_returns_429_after_10_requests():
|
|||
async def test_signal_first_10_requests_are_allowed():
|
||||
"""First 10 POST /api/signal requests from the same IP must all return 200."""
|
||||
async with make_app_client() as client:
|
||||
await client.post("/api/register", json={"uuid": _UUID_SIG_OK, "name": "OK"})
|
||||
api_key = await _register_and_get_key(client, _UUID_SIG_OK, "OK")
|
||||
|
||||
payload = {"user_id": _UUID_SIG_OK, "timestamp": 1742478000000}
|
||||
ip_hdrs = {"X-Real-IP": "6.6.6.6"}
|
||||
ip_hdrs = {"X-Real-IP": "6.6.6.6", "Authorization": f"Bearer {api_key}"}
|
||||
|
||||
statuses = []
|
||||
for _ in range(10):
|
||||
|
|
@ -162,26 +172,28 @@ async def test_signal_rate_limit_does_not_affect_register_counter():
|
|||
to return 429 — the counters use different keys ('sig:IP' vs 'IP').
|
||||
"""
|
||||
async with make_app_client() as client:
|
||||
ip_hdrs = {"X-Real-IP": "7.7.7.7"}
|
||||
ip_hdrs_reg = {"X-Real-IP": "7.7.7.7"}
|
||||
|
||||
# Register a user (increments register counter, key='7.7.7.7', count=1)
|
||||
r_reg = await client.post(
|
||||
"/api/register",
|
||||
json={"uuid": _UUID_IND_SIG, "name": "Ind"},
|
||||
headers=ip_hdrs,
|
||||
headers=ip_hdrs_reg,
|
||||
)
|
||||
assert r_reg.status_code == 200
|
||||
api_key = r_reg.json()["api_key"]
|
||||
|
||||
# Exhaust signal rate limit for same IP (11 requests, key='sig:7.7.7.7')
|
||||
payload = {"user_id": _UUID_IND_SIG, "timestamp": 1742478000000}
|
||||
ip_hdrs_sig = {"X-Real-IP": "7.7.7.7", "Authorization": f"Bearer {api_key}"}
|
||||
for _ in range(11):
|
||||
await client.post("/api/signal", json=payload, headers=ip_hdrs)
|
||||
await client.post("/api/signal", json=payload, headers=ip_hdrs_sig)
|
||||
|
||||
# Register counter is still at 1 — must allow another registration
|
||||
r_reg2 = await client.post(
|
||||
"/api/register",
|
||||
json={"uuid": _UUID_IND_SIG2, "name": "Ind2"},
|
||||
headers=ip_hdrs,
|
||||
headers=ip_hdrs_reg,
|
||||
)
|
||||
|
||||
assert r_reg2.status_code == 200, (
|
||||
|
|
@ -206,21 +218,32 @@ async def test_register_rate_limit_does_not_affect_signal_counter():
|
|||
headers=ip_hdrs,
|
||||
)
|
||||
assert r0.status_code == 200
|
||||
api_key = r0.json()["api_key"]
|
||||
|
||||
# Send 5 more register requests from the same IP to exhaust the limit
|
||||
# (register limit = 5/600s, so request #6 → 429)
|
||||
for _ in range(5):
|
||||
await client.post(
|
||||
# Send 4 more register requests from the same IP (requests 2-5 succeed,
|
||||
# each rotates the api_key; request 6 would be 429).
|
||||
# We keep track of the last api_key since re-registration rotates it.
|
||||
for _ in range(4):
|
||||
r = await client.post(
|
||||
"/api/register",
|
||||
json={"uuid": _UUID_IND_REG, "name": "Reg"},
|
||||
headers=ip_hdrs,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
api_key = r.json()["api_key"]
|
||||
|
||||
# 6th request → 429 (exhausts limit without rotating key)
|
||||
await client.post(
|
||||
"/api/register",
|
||||
json={"uuid": _UUID_IND_REG, "name": "Reg"},
|
||||
headers=ip_hdrs,
|
||||
)
|
||||
|
||||
# Signal must still succeed — signal counter (key='sig:8.8.8.8') is still 0
|
||||
r_sig = await client.post(
|
||||
"/api/signal",
|
||||
json={"user_id": _UUID_IND_REG, "timestamp": 1742478000000},
|
||||
headers=ip_hdrs,
|
||||
headers={"X-Real-IP": "8.8.8.8", "Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
|
||||
assert r_sig.status_code == 200, (
|
||||
|
|
@ -238,22 +261,22 @@ async def test_signal_rate_limit_is_per_ip_different_ips_are_independent():
|
|||
Rate limit counters are per-IP — exhausting for IP A must not block IP B.
|
||||
"""
|
||||
async with make_app_client() as client:
|
||||
await client.post("/api/register", json={"uuid": _UUID_IP_A, "name": "IPA"})
|
||||
await client.post("/api/register", json={"uuid": _UUID_IP_B, "name": "IPB"})
|
||||
api_key_a = await _register_and_get_key(client, _UUID_IP_A, "IPA")
|
||||
api_key_b = await _register_and_get_key(client, _UUID_IP_B, "IPB")
|
||||
|
||||
# Exhaust rate limit for IP A (11 requests → 11th is 429)
|
||||
for _ in range(11):
|
||||
await client.post(
|
||||
"/api/signal",
|
||||
json={"user_id": _UUID_IP_A, "timestamp": 1742478000000},
|
||||
headers={"X-Real-IP": "11.11.11.11"},
|
||||
headers={"X-Real-IP": "11.11.11.11", "Authorization": f"Bearer {api_key_a}"},
|
||||
)
|
||||
|
||||
# IP B should still be allowed (independent counter)
|
||||
r = await client.post(
|
||||
"/api/signal",
|
||||
json={"user_id": _UUID_IP_B, "timestamp": 1742478000000},
|
||||
headers={"X-Real-IP": "22.22.22.22"},
|
||||
headers={"X-Real-IP": "22.22.22.22", "Authorization": f"Bearer {api_key_b}"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200, f"IP B was incorrectly blocked: {r.status_code}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue