infrasynth-backend-kit/tests/test_security/test_auth.py
2026-08-28 14:38:47 -05:00

275 lines
11 KiB
Python

from datetime import timedelta
import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient, APIRequestFactory
from infrasynth.security.auth.api_keys import APIKeyAuthentication, SystemUser
from infrasynth.security.auth.backends import EmailOrUsernameBackend
from infrasynth.security.auth.cookies import CookieJWTAuthentication
from infrasynth.security.models import APIKey
from infrasynth.shared.crypto import encrypt
UserModel = get_user_model()
class TestEmailOrUsernameBackend:
def test_authenticates_by_username(self, user):
backend = EmailOrUsernameBackend()
result = backend.authenticate(None, username="testuser", password="testpass123")
assert result == user
def test_authenticates_by_email(self, user):
backend = EmailOrUsernameBackend()
result = backend.authenticate(None, username="test@example.com", password="testpass123")
assert result == user
def test_wrong_password_returns_none(self, user):
backend = EmailOrUsernameBackend()
assert backend.authenticate(None, username="testuser", password="wrong") is None
def test_unknown_user_returns_none(self, user):
backend = EmailOrUsernameBackend()
assert backend.authenticate(None, username="ghost", password="testpass123") is None
def test_inactive_user_returns_none(self, db):
UserModel.objects.create_user(
username="inactive", email="inactive@example.com", password="testpass123", is_active=False
)
backend = EmailOrUsernameBackend()
assert backend.authenticate(None, username="inactive", password="testpass123") is None
def test_missing_credentials_returns_none(self, user):
backend = EmailOrUsernameBackend()
assert backend.authenticate(None, username=None, password=None) is None
class TestCookieJWTAuthentication:
def test_authenticates_with_valid_cookie(self, user):
from rest_framework_simplejwt.tokens import RefreshToken
token = str(RefreshToken.for_user(user).access_token)
encrypted = encrypt(token)
request = APIRequestFactory().get("/")
request.COOKIES["access_token"] = encrypted
auth = CookieJWTAuthentication()
authenticated_user, validated = auth.authenticate(request)
assert authenticated_user == user
assert validated
def test_no_cookie_returns_none(self, user):
request = APIRequestFactory().get("/")
auth = CookieJWTAuthentication()
assert auth.authenticate(request) is None
def test_invalid_cookie_raises(self, user):
from rest_framework.exceptions import AuthenticationFailed
request = APIRequestFactory().get("/")
request.COOKIES["access_token"] = "not-a-valid-token"
auth = CookieJWTAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
class TestAPIKeyAuthentication:
@pytest.fixture
def api_key(self, db, user):
secret = "s3cr3t-api-secret"
prefix = "testkey1"
return APIKey.objects.create(
name="Test Key",
prefix=prefix,
key_hash=make_password(secret),
scopes=["read:users", "write:billing"],
created_by=user,
)
def test_authenticates_with_valid_key(self, api_key):
request = APIRequestFactory().get("/", HTTP_X_API_KEY="testkey1.s3cr3t-api-secret")
auth = APIKeyAuthentication()
auth_user, key = auth.authenticate(request)
assert isinstance(auth_user, SystemUser)
assert auth_user.scopes == ["read:users", "write:billing"]
assert key == api_key
def test_no_header_returns_none(self, api_key):
request = APIRequestFactory().get("/")
auth = APIKeyAuthentication()
assert auth.authenticate(request) is None
def test_invalid_format_raises(self, api_key):
from rest_framework.exceptions import AuthenticationFailed
request = APIRequestFactory().get("/", HTTP_X_API_KEY="no-dot-here")
auth = APIKeyAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
def test_unknown_prefix_raises(self, api_key):
from rest_framework.exceptions import AuthenticationFailed
request = APIRequestFactory().get("/", HTTP_X_API_KEY="unknownkey.secret")
auth = APIKeyAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
def test_wrong_secret_raises(self, api_key):
from rest_framework.exceptions import AuthenticationFailed
request = APIRequestFactory().get("/", HTTP_X_API_KEY="testkey1.wrongsecret")
auth = APIKeyAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
def test_inactive_key_raises(self, api_key):
from rest_framework.exceptions import AuthenticationFailed
api_key.is_active = False
api_key.save()
request = APIRequestFactory().get("/", HTTP_X_API_KEY="testkey1.s3cr3t-api-secret")
auth = APIKeyAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
def test_expired_key_raises(self, api_key):
from rest_framework.exceptions import AuthenticationFailed
api_key.expires_at = timezone.now() - timedelta(days=1)
api_key.save()
request = APIRequestFactory().get("/", HTTP_X_API_KEY="testkey1.s3cr3t-api-secret")
auth = APIKeyAuthentication()
with pytest.raises(AuthenticationFailed):
auth.authenticate(request)
def test_updates_last_used_at(self, api_key):
request = APIRequestFactory().get("/", HTTP_X_API_KEY="testkey1.s3cr3t-api-secret")
auth = APIKeyAuthentication()
auth.authenticate(request)
api_key.refresh_from_db()
assert api_key.last_used_at is not None
class TestSystemUser:
def test_scopes_and_auth_flags(self):
sys_user = SystemUser(scopes=["read:x"])
assert sys_user.is_authenticated is True
assert sys_user.is_anonymous is False
assert sys_user.is_superuser is False
assert sys_user.scopes == ["read:x"]
def test_default_scopes(self):
assert SystemUser().scopes == []
class TestAuthFlow:
def test_login_sets_cookies(self, db, user):
client = APIClient()
resp = client.post(
"/api/auth/login/",
{"username": "testuser", "password": "testpass123"},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
assert "access_token" in resp.cookies
assert "refresh_token" in resp.cookies
def test_login_by_email(self, db, user):
client = APIClient()
resp = client.post(
"/api/auth/login/",
{"username": "test@example.com", "password": "testpass123"},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
def test_login_invalid_credentials(self, db, user):
client = APIClient()
resp = client.post(
"/api/auth/login/",
{"username": "testuser", "password": "wrongpass"},
format="json",
)
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
def test_login_inactive_user(self, db):
UserModel.objects.create_user(
username="banned", email="banned@example.com", password="testpass123", is_active=False
)
client = APIClient()
resp = client.post("/api/auth/login/", {"username": "banned", "password": "testpass123"}, format="json")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
def test_check_authenticated(self, authenticated_client, user):
resp = authenticated_client.get("/api/auth/check/")
assert resp.status_code == status.HTTP_200_OK
data = resp.json()
assert data["id"] == user.pk
assert data["email"] == user.email
assert "effective_permissions" in data
def test_check_superuser_has_wildcard(self, admin_client, admin_user):
resp = admin_client.get("/api/auth/check/")
data = resp.json()
assert data["effective_permissions"] == ["*"]
def test_check_unauthenticated(self, api_client):
resp = api_client.get("/api/auth/check/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
assert resp.json() == {"authenticated": False}
def test_refresh_with_valid_cookie(self, db, user):
from rest_framework_simplejwt.tokens import RefreshToken
refresh = str(RefreshToken.for_user(user))
client = APIClient()
client.cookies["refresh_token"] = encrypt(refresh)
resp = client.post("/api/auth/refresh/")
assert resp.status_code == status.HTTP_200_OK
assert "access_token" in resp.cookies
def test_refresh_without_cookie(self, api_client):
resp = api_client.post("/api/auth/refresh/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
def test_refresh_invalid_cookie(self, api_client):
api_client.cookies["refresh_token"] = "garbage-token"
resp = api_client.post("/api/auth/refresh/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
def test_logout_clears_cookies(self, authenticated_client):
resp = authenticated_client.post("/api/auth/logout/")
assert resp.status_code == status.HTTP_200_OK
assert resp.cookies["access_token"].value == ""
assert resp.cookies["refresh_token"].value == ""
@pytest.mark.parametrize("crypto_key", [None, ""])
def test_login_falls_back_to_plaintext_tokens(self, db, user, settings, crypto_key):
settings.INFRASYNTH_SECURITY = {
**settings.INFRASYNTH_SECURITY,
"CRYPTO_KEY": crypto_key,
}
client = APIClient()
resp = client.post(
"/api/auth/login/",
{"username": "testuser", "password": "testpass123"},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
assert resp.cookies["access_token"].value
def test_full_login_roundtrip(self, db, user):
client = APIClient()
resp = client.post(
"/api/auth/login/",
{"username": "testuser", "password": "testpass123"},
format="json",
)
assert resp.status_code == status.HTTP_200_OK
check = client.get("/api/auth/check/")
assert check.status_code == status.HTTP_200_OK
assert check.json()["id"] == user.pk