Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
152
backend/tests/test_auth_sessions.py
Normal file
152
backend/tests/test_auth_sessions.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from src.auth.jwt import create_token_pair, decode_token_unsafe
|
||||
from src.models.entities import UserDB
|
||||
import src.services.session_service as session_service_module
|
||||
from src.services.session_service import SessionService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
return engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_service(engine, monkeypatch):
|
||||
monkeypatch.setattr(session_service_module, "engine", engine)
|
||||
return SessionService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_id(engine):
|
||||
now = datetime.now().timestamp()
|
||||
user_id = "user-test"
|
||||
user = UserDB(
|
||||
id=user_id,
|
||||
username="session_user",
|
||||
email="session@example.com",
|
||||
password_hash="hashed-password",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
permissions=[],
|
||||
roles=["user"],
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def test_create_token_pair_includes_session_claims():
|
||||
tokens = create_token_pair(
|
||||
user_id="user-1",
|
||||
scopes=["user"],
|
||||
session_id="session-1",
|
||||
session_family_id="family-1",
|
||||
)
|
||||
|
||||
access_payload = decode_token_unsafe(tokens.access_token)
|
||||
refresh_payload = decode_token_unsafe(tokens.refresh_token)
|
||||
|
||||
assert tokens.session_id == "session-1"
|
||||
assert tokens.session_family_id == "family-1"
|
||||
assert access_payload["sid"] == "session-1"
|
||||
assert refresh_payload["sid"] == "session-1"
|
||||
assert refresh_payload["sfid"] == "family-1"
|
||||
|
||||
|
||||
def test_session_service_create_and_validate_refresh_token(session_service, user_id):
|
||||
created = session_service.create_session(
|
||||
user_id=user_id,
|
||||
refresh_token="refresh-token-1",
|
||||
session_id="session-1",
|
||||
session_family_id="family-1",
|
||||
)
|
||||
|
||||
validated = session_service.validate_refresh_token(created.id, "refresh-token-1")
|
||||
|
||||
assert created.id == "session-1"
|
||||
assert created.session_family_id == "family-1"
|
||||
assert validated is not None
|
||||
assert validated.id == created.id
|
||||
assert session_service.is_session_active(created.id) is True
|
||||
|
||||
|
||||
def test_rotate_refresh_token_revokes_previous_session(session_service, user_id):
|
||||
created = session_service.create_session(
|
||||
user_id=user_id,
|
||||
refresh_token="refresh-token-1",
|
||||
session_id="session-1",
|
||||
session_family_id="family-1",
|
||||
)
|
||||
|
||||
rotated = session_service.rotate_refresh_token(
|
||||
created.id,
|
||||
"refresh-token-1",
|
||||
"refresh-token-2",
|
||||
new_session_id="session-2",
|
||||
)
|
||||
|
||||
previous = session_service.get_session(created.id)
|
||||
|
||||
assert rotated is not None
|
||||
assert rotated.id == "session-2"
|
||||
assert rotated.session_family_id == "family-1"
|
||||
assert previous is not None
|
||||
assert previous.status == "rotated"
|
||||
assert previous.replaced_by_session_id == "session-2"
|
||||
assert session_service.validate_refresh_token("session-2", "refresh-token-2") is not None
|
||||
|
||||
|
||||
def test_refresh_token_reuse_revokes_session_family(session_service, user_id):
|
||||
created = session_service.create_session(
|
||||
user_id=user_id,
|
||||
refresh_token="refresh-token-1",
|
||||
session_id="session-1",
|
||||
session_family_id="family-1",
|
||||
)
|
||||
rotated = session_service.rotate_refresh_token(
|
||||
created.id,
|
||||
"refresh-token-1",
|
||||
"refresh-token-2",
|
||||
new_session_id="session-2",
|
||||
)
|
||||
|
||||
invalidated = session_service.validate_refresh_token("session-2", "wrong-refresh-token")
|
||||
active_sessions = session_service.list_user_sessions(user_id, include_inactive=True)
|
||||
|
||||
assert rotated is not None
|
||||
assert invalidated is None
|
||||
assert all(session.status in {"rotated", "revoked"} for session in active_sessions)
|
||||
assert any(session.id == "session-2" and session.status == "revoked" for session in active_sessions)
|
||||
|
||||
|
||||
def test_expired_session_is_not_active(session_service, user_id):
|
||||
expired_session = session_service.create_session(
|
||||
user_id=user_id,
|
||||
refresh_token="refresh-token-expired",
|
||||
session_id="session-expired",
|
||||
expires_at=(datetime.now() - timedelta(minutes=1)).timestamp(),
|
||||
)
|
||||
|
||||
validated = session_service.validate_refresh_token(expired_session.id, "refresh-token-expired")
|
||||
expired = session_service.get_session(expired_session.id)
|
||||
|
||||
assert validated is None
|
||||
assert expired is not None
|
||||
assert expired.status == "revoked"
|
||||
assert expired.revoked_reason == "expired"
|
||||
Reference in New Issue
Block a user