release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+210
View File
@@ -0,0 +1,210 @@
from __future__ import annotations
import asyncio
import uuid
from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.models.permission_access_log import PermissionAccessLog
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
from app.models.security_access_log import SecurityAccessLog
from app.models.study import Study
from app.models.user import User, UserStatus
from app.services import monitoring_retention
from app.services import permission_log_writer, security_access_log_writer
class _FakeSession:
def __init__(self) -> None:
self.added: list[object] = []
self.committed = False
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
def add(self, value: object) -> None:
self.added.append(value)
async def commit(self) -> None:
self.committed = True
@pytest.mark.asyncio
async def test_permission_writer_gracefully_flushes_and_reports_stats(monkeypatch):
session = _FakeSession()
monkeypatch.setattr(permission_log_writer, "SessionLocal", lambda: session)
writer = permission_log_writer.PermissionLogWriter()
await writer.start()
writer.enqueue(
{
"study_id": uuid.uuid4(),
"user_id": uuid.uuid4(),
"endpoint_key": "subjects.read",
"role": "PM",
"allowed": True,
"elapsed_ms": 2.5,
}
)
await writer.stop()
stats = writer.stats()
assert session.committed is True
assert len(session.added) == 1
assert stats["running"] is False
assert stats["accepted_entries"] == 1
assert stats["written_entries"] == 1
assert stats["dropped_entries"] == 0
assert stats["queue_size"] == 0
assert stats["last_success_at"] is not None
@pytest.mark.asyncio
async def test_permission_writer_retries_and_exposes_permanent_failure(monkeypatch):
class _FailingSession:
async def __aenter__(self):
raise RuntimeError("database unavailable")
async def __aexit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr(permission_log_writer, "SessionLocal", _FailingSession)
writer = permission_log_writer.PermissionLogWriter()
batch = [
{
"study_id": uuid.uuid4(),
"user_id": uuid.uuid4(),
"endpoint_key": "subjects.read",
"role": "PM",
"allowed": True,
"elapsed_ms": 2.5,
}
]
assert await writer._write_batch(batch) is False
stats = writer.stats()
assert stats["retry_count"] == 1
assert stats["failed_batches"] == 1
assert stats["failed_entries"] == 1
assert stats["last_error_type"] == "RuntimeError"
def test_writer_queue_overflow_is_counted():
writer = security_access_log_writer.SecurityAccessLogWriter()
writer._queue = asyncio.Queue(maxsize=1)
writer.enqueue({"path": "/first"})
writer.enqueue({"path": "/dropped"})
stats = writer.stats()
assert stats["accepted_entries"] == 1
assert stats["dropped_entries"] == 1
assert stats["queue_size"] == 1
@pytest.mark.asyncio
async def test_retention_deletes_expired_rows_and_preserves_recent_rows(
db_session, test_engine, monkeypatch
):
now = datetime(2026, 7, 10, tzinfo=timezone.utc)
study_id = (await db_session.execute(select(Study.id).limit(1))).scalar_one()
user = User(
email=f"retention-{uuid.uuid4()}@example.com",
password_hash="hash",
full_name="Retention Test",
clinical_department="QA",
status=UserStatus.ACTIVE,
)
db_session.add(user)
await db_session.flush()
old_permission = PermissionAccessLog(
study_id=study_id,
user_id=user.id,
endpoint_key="old",
role="PM",
allowed=True,
elapsed_ms=1,
created_at=now - timedelta(days=91),
)
recent_permission = PermissionAccessLog(
study_id=study_id,
user_id=user.id,
endpoint_key="recent",
role="PM",
allowed=True,
elapsed_ms=1,
created_at=now - timedelta(days=89),
)
old_security = SecurityAccessLog(
method="GET",
path="/old",
status_code=404,
elapsed_ms=1,
auth_status="ANONYMOUS",
created_at=now - timedelta(days=91),
)
recent_security = SecurityAccessLog(
method="GET",
path="/recent",
status_code=200,
elapsed_ms=1,
auth_status="AUTHENTICATED",
created_at=now - timedelta(days=89),
)
old_metric = PermissionMetricSnapshot(
bucket_time=now - timedelta(days=401)
)
recent_metric = PermissionMetricSnapshot(
bucket_time=now - timedelta(days=399)
)
db_session.add_all(
[
old_permission,
recent_permission,
old_security,
recent_security,
old_metric,
recent_metric,
]
)
await db_session.commit()
old_permission_id = old_permission.id
recent_permission_id = recent_permission.id
old_security_id = old_security.id
recent_security_id = recent_security.id
old_metric_id = old_metric.id
recent_metric_id = recent_metric.id
session_factory = async_sessionmaker(
bind=test_engine,
class_=AsyncSession,
expire_on_commit=False,
)
monkeypatch.setattr(monitoring_retention, "SessionLocal", session_factory)
monkeypatch.setattr(
monitoring_retention.settings, "MONITORING_ACCESS_LOG_RETENTION_DAYS", 90
)
monkeypatch.setattr(
monitoring_retention.settings, "MONITORING_METRIC_RETENTION_DAYS", 400
)
deleted = await monitoring_retention.purge_expired_monitoring_data(now=now)
db_session.expire_all()
assert deleted["permission_access_logs"] >= 1
assert deleted["security_access_logs"] >= 1
assert deleted["permission_metric_snapshots"] >= 1
assert await db_session.get(PermissionAccessLog, old_permission_id) is None
assert await db_session.get(SecurityAccessLog, old_security_id) is None
assert await db_session.get(PermissionMetricSnapshot, old_metric_id) is None
assert await db_session.get(PermissionAccessLog, recent_permission_id) is not None
assert await db_session.get(SecurityAccessLog, recent_security_id) is not None
assert await db_session.get(PermissionMetricSnapshot, recent_metric_id) is not None