- 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()
153 lines
4.7 KiB
Python
153 lines
4.7 KiB
Python
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"
|