bio-nexus-api / tests /test_auth.py
Samad14's picture
Upload folder using huggingface_hub
f783f69 verified
Raw History Blame Contribute Delete
3.75 kB
"""Auth verification tests — forged JWTs must never yield a user_id."""
import asyncio
import time
import jwt as pyjwt
import pytest
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
from app.services import auth
SECRET = "unit-test-secret-0123456789abcdef-0123456789"
SUB = "11111111-2222-3333-4444-555555555555"
def _make_token(sub=SUB, secret=SECRET, expires_delta=3600, headers=None):
payload = {
"sub": sub,
"role": "authenticated",
"exp": int(time.time()) + expires_delta,
}
return pyjwt.encode(payload, secret, algorithm="HS256", headers=headers or {})
def _creds(token: str) -> HTTPAuthorizationCredentials:
return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
@pytest.fixture(autouse=True)
def _reset_state(monkeypatch):
monkeypatch.delenv("SUPABASE_JWT_SECRET", raising=False)
auth._verified_cache.clear()
yield
auth._verified_cache.clear()
class TestLocalVerification:
def test_valid_signed_token_returns_sub(self, monkeypatch):
monkeypatch.setenv("SUPABASE_JWT_SECRET", SECRET)
assert auth._verify_locally(_make_token()) == SUB
def test_forged_signature_rejected(self, monkeypatch):
monkeypatch.setenv("SUPABASE_JWT_SECRET", SECRET)
forged = _make_token(secret="attacker-key-0123456789abcdef-0123456789")
assert auth._verify_locally(forged) is None
def test_expired_token_rejected(self, monkeypatch):
monkeypatch.setenv("SUPABASE_JWT_SECRET", SECRET)
expired = _make_token(expires_delta=-10)
assert auth._verify_locally(expired) is None
def test_missing_claims_rejected(self, monkeypatch):
monkeypatch.setenv("SUPABASE_JWT_SECRET", SECRET)
token = pyjwt.encode({"role": "authenticated"}, SECRET, algorithm="HS256")
assert auth._verify_locally(token) is None
class TestGetUserId:
def test_anonymous_when_no_credentials(self):
assert asyncio.run(auth.get_user_id(None)) is None
@pytest.mark.parametrize("bad", ["", "not-a-jwt", "a.b", "...."])
def test_malformed_tokens_rejected_without_network(self, bad):
# No SUPABASE_JWT_SECRET set -> would hit network path if it got there;
# malformed shapes must short-circuit to None.
assert asyncio.run(auth.get_user_id(_creds(bad))) is None
def test_forged_token_rejected_with_secret_configured(self, monkeypatch):
monkeypatch.setenv("SUPABASE_JWT_SECRET", SECRET)
forged = f"{_make_token().split('.')[0]}.{_make_token().split('.')[1]}.garbage"
assert asyncio.run(auth.get_user_id(_creds(forged))) is None
def test_delegates_to_supabase_path_without_secret(self, monkeypatch):
seen = {}
async def fake_verify(token: str):
seen["token"] = token
return SUB
monkeypatch.setattr(auth, "_verify_via_supabase", fake_verify)
result = asyncio.run(auth.get_user_id(_creds("aaa.bbb.ccc")))
assert result == SUB
assert seen["token"] == "aaa.bbb.ccc"
class TestRequireUserId:
def test_raises_401_on_unverifiable_token(self, monkeypatch):
async def fake_verify(token: str):
return None
monkeypatch.setattr(auth, "_verify_via_supabase", fake_verify)
with pytest.raises(HTTPException) as exc:
asyncio.run(auth.require_user_id(_creds("x.y.z")))
assert exc.value.status_code == 401
def test_passes_through_verified_sub(self, monkeypatch):
async def fake_verify(token: str):
return SUB
monkeypatch.setattr(auth, "_verify_via_supabase", fake_verify)
assert asyncio.run(auth.require_user_id(_creds("x.y.z"))) == SUB