80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""Tests for refresh token uniqueness (jti claim) and model column length."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
from jose import jwt
|
|
|
|
from app.config import settings
|
|
from app.routers.auth import _create_refresh_token
|
|
from app.models.session import RefreshToken
|
|
|
|
|
|
class TestRefreshTokenJtiUniqueness:
|
|
"""Verify that _create_refresh_token produces unique tokens even when
|
|
called with identical data in rapid succession (issue #1 from review)."""
|
|
|
|
def test_two_calls_produce_different_tokens(self):
|
|
"""Two calls with the same sub produce different JWTs because of jti."""
|
|
token_a = _create_refresh_token({"sub": "42"})
|
|
token_b = _create_refresh_token({"sub": "42"})
|
|
assert token_a != token_b
|
|
|
|
def test_jti_is_valid_uuid(self):
|
|
"""The jti claim inside the refresh token is a valid UUID."""
|
|
token = _create_refresh_token({"sub": "1"})
|
|
payload = jwt.decode(
|
|
token,
|
|
settings.public_pem,
|
|
algorithms=[settings.ALGORITHM],
|
|
)
|
|
assert "jti" in payload
|
|
# Verify it parses as a UUID (raises ValueError if not)
|
|
uuid.UUID(payload["jti"])
|
|
|
|
def test_each_token_has_unique_jti(self):
|
|
"""Generate 100 tokens for the same user — all jtis must differ."""
|
|
jtis = set()
|
|
for _ in range(100):
|
|
token = _create_refresh_token({"sub": "999"})
|
|
payload = jwt.decode(
|
|
token,
|
|
settings.public_pem,
|
|
algorithms=[settings.ALGORITHM],
|
|
)
|
|
jtis.add(payload["jti"])
|
|
assert len(jtis) == 100
|
|
|
|
def test_jti_survives_decode(self):
|
|
"""The jti claim is present and correct after decode."""
|
|
token = _create_refresh_token({"sub": "7"})
|
|
payload = jwt.decode(
|
|
token,
|
|
settings.public_pem,
|
|
algorithms=[settings.ALGORITHM],
|
|
)
|
|
assert payload["sub"] == "7"
|
|
assert "jti" in payload
|
|
assert "exp" in payload
|
|
|
|
|
|
class TestRefreshTokenModelColumnLength:
|
|
"""Verify the model's token column length matches the migration (issue #2)."""
|
|
|
|
def test_token_column_has_length_512(self):
|
|
"""RefreshToken.token column should be String(512), not unbounded."""
|
|
from sqlalchemy import inspect
|
|
|
|
# Get the column definition from the model's table
|
|
column = RefreshToken.__table__.columns["token"]
|
|
# The type should be String with length 512
|
|
assert column.type.length == 512
|
|
|
|
def test_token_column_type_is_string(self):
|
|
"""The token column should be a String type."""
|
|
from sqlalchemy import String as SAString
|
|
|
|
column = RefreshToken.__table__.columns["token"]
|
|
assert isinstance(column.type, SAString)
|