feat: implement refresh token functionality; update authentication and token models; add tests for refresh endpoint
Test / test (push) Successful in 13s
Test / test (push) Successful in 13s
This commit is contained in:
@@ -30,6 +30,7 @@ async def test_register_user_creates_organization_membership(
|
||||
body = response.json()
|
||||
assert body["token_type"] == "bearer"
|
||||
assert "access_token" in body
|
||||
assert "refresh_token" in body
|
||||
|
||||
async with session_factory() as session:
|
||||
user = await session.scalar(select(User).where(User.email == payload["email"]))
|
||||
@@ -74,6 +75,7 @@ async def test_login_endpoint_returns_token_for_valid_credentials(
|
||||
body = response.json()
|
||||
assert body["token_type"] == "bearer"
|
||||
assert "access_token" in body
|
||||
assert "refresh_token" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -98,3 +100,47 @@ async def test_token_endpoint_rejects_invalid_credentials(
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Invalid email or password"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_endpoint_returns_new_tokens(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
async with session_factory() as session:
|
||||
user = User(
|
||||
email="refresh-user@example.com",
|
||||
hashed_password=password_hasher.hash("StrongPass123"),
|
||||
name="Refresh User",
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
|
||||
login_response = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "refresh-user@example.com", "password": "StrongPass123"},
|
||||
)
|
||||
assert login_response.status_code == 200
|
||||
refresh_token = login_response.json()["refresh_token"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": refresh_token},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert "access_token" in body
|
||||
assert "refresh_token" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_endpoint_rejects_invalid_token(client: AsyncClient) -> None:
|
||||
response = await client.post(
|
||||
"/api/v1/auth/refresh",
|
||||
json={"refresh_token": "not-a-jwt"},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Invalid refresh token"
|
||||
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.security import JWTService, PasswordHasher
|
||||
from app.models.user import User
|
||||
from app.repositories.user_repo import UserRepository
|
||||
from app.services.auth_service import AuthService, InvalidCredentialsError
|
||||
from app.services.auth_service import AuthService, InvalidCredentialsError, InvalidRefreshTokenError
|
||||
|
||||
|
||||
class StubUserRepository(UserRepository):
|
||||
@@ -25,6 +25,11 @@ class StubUserRepository(UserRepository):
|
||||
return self._user
|
||||
return None
|
||||
|
||||
async def get_by_id(self, user_id: int) -> User | None: # pragma: no cover - helper
|
||||
if self._user and self._user.id == user_id:
|
||||
return self._user
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def password_hasher() -> PasswordHasher:
|
||||
@@ -71,7 +76,7 @@ async def test_authenticate_invalid_credentials(
|
||||
await service.authenticate("user@example.com", "wrong-pass")
|
||||
|
||||
|
||||
def test_create_access_token_contains_user_claims(
|
||||
def test_issue_tokens_contains_user_claims(
|
||||
password_hasher: PasswordHasher,
|
||||
jwt_service: JWTService,
|
||||
) -> None:
|
||||
@@ -79,9 +84,43 @@ def test_create_access_token_contains_user_claims(
|
||||
user.id = 42
|
||||
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
||||
|
||||
token = service.create_access_token(user)
|
||||
payload = jwt_service.decode(token.access_token)
|
||||
token_pair = service.issue_tokens(user)
|
||||
payload = jwt_service.decode(token_pair.access_token)
|
||||
|
||||
assert payload["sub"] == str(user.id)
|
||||
assert payload["email"] == user.email
|
||||
assert token.expires_in > 0
|
||||
assert payload["scope"] == "access"
|
||||
assert token_pair.refresh_token
|
||||
assert token_pair.expires_in > 0
|
||||
assert token_pair.refresh_expires_in > token_pair.expires_in
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_tokens_returns_new_pair(
|
||||
password_hasher: PasswordHasher,
|
||||
jwt_service: JWTService,
|
||||
) -> None:
|
||||
user = User(email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True)
|
||||
user.id = 7
|
||||
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
||||
|
||||
initial = service.issue_tokens(user)
|
||||
refreshed = await service.refresh_tokens(initial.refresh_token)
|
||||
|
||||
assert refreshed.access_token
|
||||
assert refreshed.refresh_token
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_tokens_rejects_access_token(
|
||||
password_hasher: PasswordHasher,
|
||||
jwt_service: JWTService,
|
||||
) -> None:
|
||||
user = User(email="refresh@example.com", hashed_password="hashed", name="Refresh", is_active=True)
|
||||
user.id = 9
|
||||
service = AuthService(StubUserRepository(user), password_hasher, jwt_service)
|
||||
|
||||
pair = service.issue_tokens(user)
|
||||
|
||||
with pytest.raises(InvalidRefreshTokenError):
|
||||
await service.refresh_tokens(pair.access_token)
|
||||
|
||||
Reference in New Issue
Block a user