诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""P1-3 · 审计保留回归:超窗删/窗内留/dry-run/yes 门禁/分批/API 正常。"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
def _make_log(tenant, days_ago, action="view"):
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
log = AuditLog.objects.create(
|
||||
tenant=tenant, action=action, target_type="Product", target_id="1",
|
||||
)
|
||||
AuditLog.objects.filter(pk=log.pk).update(
|
||||
created_at=timezone.now() - timedelta(days=days_ago)
|
||||
)
|
||||
return log
|
||||
|
||||
|
||||
def test_dry_run_deletes_nothing(db, tenant):
|
||||
_make_log(tenant, 400)
|
||||
before = __import__("apps.core.models", fromlist=["AuditLog"]).AuditLog.objects.count()
|
||||
call_command("purge_audit_logs", days=180, dry_run=True)
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
assert AuditLog.objects.count() == before
|
||||
|
||||
|
||||
def test_without_yes_deletes_nothing(db, tenant):
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
_make_log(tenant, 400)
|
||||
before = AuditLog.objects.count()
|
||||
call_command("purge_audit_logs", days=180)
|
||||
assert AuditLog.objects.count() == before
|
||||
|
||||
|
||||
def test_window_boundary(db, tenant):
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
old = _make_log(tenant, 400)
|
||||
fresh = _make_log(tenant, 10)
|
||||
call_command("purge_audit_logs", days=180, yes=True)
|
||||
ids = set(AuditLog.objects.values_list("id", flat=True))
|
||||
assert old.id not in ids
|
||||
assert fresh.id in ids
|
||||
|
||||
|
||||
def test_batch_and_export(db, tenant, tmp_path):
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
for _ in range(5):
|
||||
_make_log(tenant, 400)
|
||||
out = tmp_path / "audit.jsonl"
|
||||
call_command("purge_audit_logs", days=180, yes=True, batch=2,
|
||||
export=str(out))
|
||||
assert AuditLog.objects.filter(
|
||||
created_at__lt=timezone.now() - timedelta(days=180)).count() == 0
|
||||
lines = out.read_text(encoding="utf-8").strip().split("\n")
|
||||
assert len(lines) == 5
|
||||
row = json.loads(lines[0])
|
||||
assert row["action"] == "view" and "created_at" in row
|
||||
|
||||
|
||||
def test_purge_actor_registered_and_runs(db, tenant):
|
||||
"""P1-3 定时接线:core.tasks actor 可直接调用,超窗日志被删。"""
|
||||
from apps.core import tasks as core_tasks
|
||||
|
||||
_make_log(tenant, 400)
|
||||
fresh = _make_log(tenant, 5)
|
||||
core_tasks.purge_expired_audit_logs()
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
ids = set(AuditLog.objects.values_list("id", flat=True))
|
||||
assert fresh.id in ids
|
||||
assert AuditLog.objects.filter(
|
||||
created_at__lt=timezone.now() - timedelta(days=180)).count() == 0
|
||||
|
||||
|
||||
def test_audit_api_still_paginates_after_purge(db, tenant, user):
|
||||
from apps.core.models import AuditLog
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
for _ in range(3):
|
||||
_make_log(tenant, 400)
|
||||
_make_log(tenant, 5)
|
||||
call_command("purge_audit_logs", days=180, yes=True)
|
||||
|
||||
c = APIClient()
|
||||
c.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {RefreshToken.for_user(user).access_token}",
|
||||
HTTP_X_TENANT_ID=tenant.code,
|
||||
)
|
||||
r = c.get("/api/v1/core/audit-logs/")
|
||||
assert r.status_code == 200, r.content
|
||||
body = r.json()
|
||||
assert body["count"] == AuditLog.objects.filter(tenant=tenant).count() == 1
|
||||
Reference in New Issue
Block a user