"""Tests for app.auth — JWT token creation, verification, and decoding.""" import pytest from datetime import datetime, timedelta, timezone from fastapi import HTTPException from jose import jwt from app.auth import ( create_access_token, create_download_token, verify_download_token, decode_token, ) from app.config import settings class TestCreateAccessToken: """Tests for create_access_token().""" def test_returns_string_token(self): token = create_access_token(user_id=1, is_admin=True) assert isinstance(token, str) assert len(token) > 0 def test_token_contains_correct_payload(self): token = create_access_token(user_id=42, is_admin=False) payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"]) assert payload["sub"] == "42" assert payload["is_admin"] is False assert "exp" in payload def test_admin_flag_is_preserved(self): admin_token = create_access_token(user_id=1, is_admin=True) payload = jwt.decode(admin_token, settings.JWT_SECRET_KEY, algorithms=["HS256"]) assert payload["is_admin"] is True def test_token_expires_in_configured_minutes(self): token = create_access_token(user_id=1, is_admin=True) payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"]) expected_exp = datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) # Allow 1s drift assert abs(payload["exp"] - expected_exp.timestamp()) < 2 class TestCreateDownloadToken: """Tests for create_download_token().""" def test_download_token_contains_artifact_id(self): token = create_download_token(artifact_id=99) payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"]) assert payload["sub"] == "download" assert payload["artifact_id"] == 99 def test_download_token_expires_quickly(self): token = create_download_token(artifact_id=1) payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"]) expected_exp = datetime.now(timezone.utc) + timedelta(minutes=settings.DOWNLOAD_TOKEN_MINUTES) assert abs(payload["exp"] - expected_exp.timestamp()) < 2 class TestVerifyDownloadToken: """Tests for verify_download_token().""" def test_valid_download_token_returns_artifact_id(self): token = create_download_token(artifact_id=55) result = verify_download_token(token) assert result == 55 def test_access_token_rejected_as_download_token(self): token = create_access_token(user_id=1, is_admin=True) result = verify_download_token(token) assert result is None def test_expired_token_returns_none(self): payload = { "sub": "download", "artifact_id": 1, "exp": datetime.now(timezone.utc) - timedelta(minutes=1), } expired_token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") result = verify_download_token(expired_token) assert result is None def test_tampered_token_returns_none(self): token = create_download_token(artifact_id=10) tampered = token[:-5] + "xxxxx" result = verify_download_token(tampered) assert result is None def test_wrong_secret_returns_none(self): payload = { "sub": "download", "artifact_id": 1, "exp": datetime.now(timezone.utc) + timedelta(minutes=5), } wrong_token = jwt.encode(payload, "wrong-secret", algorithm="HS256") result = verify_download_token(wrong_token) assert result is None def test_non_integer_artifact_id_returns_none(self): payload = { "sub": "download", "artifact_id": "not_an_int", "exp": datetime.now(timezone.utc) + timedelta(minutes=5), } token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") result = verify_download_token(token) assert result is None class TestDecodeToken: """Tests for decode_token().""" def test_valid_token_returns_payload(self): token = create_access_token(user_id=7, is_admin=False) payload = decode_token(token) assert payload["sub"] == "7" def test_expired_token_raises_401(self): payload = { "sub": "1", "exp": datetime.now(timezone.utc) - timedelta(hours=1), } expired = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") with pytest.raises(HTTPException) as exc_info: decode_token(expired) assert exc_info.value.status_code == 401 def test_invalid_signature_raises_401(self): with pytest.raises(HTTPException) as exc_info: decode_token("not.a.token") assert exc_info.value.status_code == 401 def test_token_without_sub_raises_401(self): payload = { "exp": datetime.now(timezone.utc) + timedelta(hours=1), } token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") with pytest.raises(HTTPException) as exc_info: decode_token(token) assert exc_info.value.status_code == 401