Files
ctms/backend/tests/test_user_login_sessions.py
T
Cheng Zhou 3e77127687
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
功能(提醒):统一项目提醒中心与桌面通知链路
增加通用提醒状态、数据库迁移和定时同步,覆盖风险时效、文件回执、项目里程碑、访视窗口与协作申请。

新增网页端和桌面端提醒中心、真实投递诊断与固定隐私通知正文,并补齐登录来源聚合、测试和说明文档。
2026-07-16 16:50:34 +08:00

225 lines
7.4 KiB
Python

import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
import pytest
from app.core.config import settings
from app.crud import user as user_crud
from app.models.user import User, UserStatus
from app.models.user_login_session import UserLoginSession
from app.schemas.user import UserLoginActivityRead
from app.services.user_login_sessions import (
create_login_session,
get_login_summaries,
login_activity_payload,
session_id_from_payload,
)
def test_legacy_session_id_is_stable_without_storing_a_token():
payload = {"sub": "00000000-0000-0000-0000-000000000101", "orig_iat": 1_700_000_000, "client_type": "web"}
first = session_id_from_payload(payload)
second = session_id_from_payload(payload)
assert first == second
assert isinstance(first, uuid.UUID)
def test_session_heartbeat_returns_server_observed_client_ip():
auth_source = (Path(__file__).resolve().parents[1] / "app" / "api" / "v1" / "auth.py").read_text()
assert "resolve_client_ip(request)" in auth_source
assert '"client_ip": resolve_client_ip(request)' in auth_source
@pytest.mark.asyncio
async def test_login_session_stores_server_observed_login_ip(db_session):
user = User(
id=uuid.uuid4(),
email="login-ip@example.com",
password_hash="hash",
full_name="Login IP",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
db_session.add(user)
await db_session.commit()
request = SimpleNamespace(
headers={
"x-ctms-client-type": "desktop",
"x-ctms-client-platform": "macos",
"x-ctms-client-version": "0.1.0",
},
client=SimpleNamespace(host="203.0.113.18"),
)
session = await create_login_session(
db_session,
session_id=uuid.uuid4(),
user_id=user.id,
request=request,
)
assert session.login_ip == "203.0.113.18"
assert session.client_type == "desktop"
def test_login_activity_payload_uses_server_authoritative_status_and_local_ip_location(monkeypatch):
now = datetime.now(timezone.utc)
monkeypatch.setattr(settings, "USER_SESSION_ONLINE_SECONDS", 300)
session = UserLoginSession(
id=uuid.uuid4(),
user_id=uuid.uuid4(),
client_type="web",
login_ip="127.0.0.1",
login_at=now - timedelta(minutes=10),
last_seen_at=now - timedelta(seconds=30),
)
online = login_activity_payload(session, reference_time=now)
assert online["activity_status"] == "ONLINE"
assert online["login_ip"] == "127.0.0.1"
assert online["ip_location"] == "本机"
assert UserLoginActivityRead.model_validate(online).activity_status == "ONLINE"
session.last_seen_at = now - timedelta(minutes=6)
assert login_activity_payload(session, reference_time=now)["activity_status"] == "OFFLINE"
session.ended_at = now - timedelta(minutes=1)
assert login_activity_payload(session, reference_time=now)["activity_status"] == "ENDED"
@pytest.mark.asyncio
async def test_login_summary_marks_recent_unended_sessions_online(db_session, monkeypatch):
now = datetime.now(timezone.utc)
monkeypatch.setattr(settings, "USER_SESSION_ONLINE_SECONDS", 300)
user = User(
id=uuid.uuid4(),
email="login-summary@example.com",
password_hash="hash",
full_name="Login Summary",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
db_session.add(user)
db_session.add_all(
[
UserLoginSession(
id=uuid.uuid4(),
user_id=user.id,
client_type="desktop",
login_at=now - timedelta(minutes=10),
last_seen_at=now - timedelta(seconds=20),
),
UserLoginSession(
id=uuid.uuid4(),
user_id=user.id,
client_type="web",
login_at=now - timedelta(days=1),
last_seen_at=now - timedelta(days=1),
ended_at=now - timedelta(days=1),
end_reason="logout",
),
]
)
await db_session.commit()
summaries = await get_login_summaries(db_session, [user.id])
assert summaries[user.id].status == "ONLINE"
assert summaries[user.id].active_session_count == 1
assert summaries[user.id].client_type == "desktop"
@pytest.mark.asyncio
async def test_login_summary_counts_same_client_and_ip_as_one_active_source(db_session, monkeypatch):
now = datetime.now(timezone.utc)
monkeypatch.setattr(settings, "USER_SESSION_ONLINE_SECONDS", 300)
user = User(
id=uuid.uuid4(),
email="deduplicated-login-source@example.com",
password_hash="hash",
full_name="Deduplicated Login Source",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
db_session.add(user)
db_session.add_all(
[
UserLoginSession(
id=uuid.uuid4(),
user_id=user.id,
client_type="web",
login_ip="192.168.97.1",
login_at=now - timedelta(minutes=2),
last_seen_at=now - timedelta(seconds=20),
),
UserLoginSession(
id=uuid.uuid4(),
user_id=user.id,
client_type="web",
login_ip="192.168.97.1",
login_at=now - timedelta(minutes=1),
last_seen_at=now - timedelta(seconds=10),
),
UserLoginSession(
id=uuid.uuid4(),
user_id=user.id,
client_type="desktop",
login_ip="192.168.97.1",
login_at=now - timedelta(minutes=1),
last_seen_at=now - timedelta(seconds=5),
),
]
)
await db_session.commit()
summaries = await get_login_summaries(db_session, [user.id])
assert summaries[user.id].status == "ONLINE"
assert summaries[user.id].active_session_count == 2
@pytest.mark.asyncio
async def test_user_list_filters_online_and_offline_accounts(db_session, monkeypatch):
now = datetime.now(timezone.utc)
monkeypatch.setattr(settings, "USER_SESSION_ONLINE_SECONDS", 300)
online_user = User(
id=uuid.uuid4(),
email="online-filter@example.com",
password_hash="hash",
full_name="Online Filter",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
offline_user = User(
id=uuid.uuid4(),
email="offline-filter@example.com",
password_hash="hash",
full_name="Offline Filter",
clinical_department="IT",
status=UserStatus.ACTIVE,
)
db_session.add_all([online_user, offline_user])
db_session.add(
UserLoginSession(
id=uuid.uuid4(),
user_id=online_user.id,
client_type="web",
login_at=now - timedelta(minutes=1),
last_seen_at=now - timedelta(seconds=10),
)
)
await db_session.commit()
online = await user_crud.list_users(db_session, login_status="ONLINE")
offline = await user_crud.list_users(db_session, login_status="OFFLINE")
assert online_user.id in {user.id for user in online}
assert offline_user.id not in {user.id for user in online}
assert offline_user.id in {user.id for user in offline}
assert await user_crud.count_users(db_session, login_status="ONLINE") == len(online)