Files
gsfdc/server/tests/test_expert_qa.py
T

182 lines
6.5 KiB
Python

"""专家答疑:会员提问、只读本人、工作人员回复、待回复计数与超时。"""
from datetime import datetime, timedelta
import pytest
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.core.security import create_access_token
from app.models.expert import is_overdue, workday_deadline
from app.services.email_code import _key
from app.services.redis_client import get_redis
@pytest.fixture
def client(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "test-secret-must-be-32-characters!")
monkeypatch.setenv("API_PAYLOAD_ENCRYPT", "false")
monkeypatch.setenv("SMTP_HOST", "")
get_settings.cache_clear()
if not get_settings().database_url.strip():
pytest.skip("DATABASE_URL 为空")
try:
get_redis().ping()
except Exception:
pytest.skip("Redis 不可达")
monkeypatch.setattr("app.services.email_code.send_email", lambda **kwargs: None)
from app.main import create_app
with TestClient(create_app()) as api:
yield api
get_settings.cache_clear()
def _minio_ready() -> bool:
try:
from app.services.storage import ensure_buckets
ensure_buckets()
return True
except Exception:
return False
def _code(purpose: str, email: str) -> str:
stored = get_redis().get(_key(purpose, email))
assert stored is not None
if isinstance(stored, bytes):
stored = stored.decode("utf-8")
return stored
def _register(client, email: str, password: str = "Passw0rd1") -> str:
assert client.post("/api/site/auth/email-code", json={"email": email, "purpose": "register"}).status_code == 200
code = _code("register", email)
registered = client.post(
"/api/site/auth/register",
json={"email": email, "code": code, "password": password, "privacyAccepted": True},
)
assert registered.status_code == 200, registered.text
return registered.json()["accessToken"]
def test_workday_deadline_skips_weekend():
# 周五提交,3 个工作日后是下周三
friday = datetime(2025, 1, 3, 10, 0, 0) # Friday
assert workday_deadline(friday, 3) == datetime(2025, 1, 8).date() # Wed
assert is_overdue(friday, 3, now=datetime(2025, 1, 8, 9, 0, 0)) is False
assert is_overdue(friday, 3, now=datetime(2025, 1, 9, 9, 0, 0)) is True
def test_expert_qa_ask_reply_isolation_and_pending_count(client):
if not _minio_ready():
pytest.skip("MinIO 不可达")
import time
email_a = f"qa-a-{int(time.time() * 1000)}@example.com"
email_b = f"qa-b-{int(time.time() * 1000)}@example.com"
token_a = _register(client, email_a)
token_b = _register(client, email_b)
headers_a = {"Authorization": f"Bearer {token_a}"}
headers_b = {"Authorization": f"Bearer {token_b}"}
staff = {"Authorization": f"Bearer {create_access_token('1', 'staff')}"}
denied = client.post(
"/api/site/member/expert-questions",
json={"title": "未登录", "body": "x", "attachmentFileIds": []},
)
assert denied.status_code in (401, 403)
uploaded = client.post(
"/api/site/member/files",
headers=headers_a,
files={"file": ("证据.pdf", b"%PDF-1.4\nqa", "application/pdf")},
)
assert uploaded.status_code == 200, uploaded.text
file_id = uploaded.json()["id"]
created = client.post(
"/api/site/member/expert-questions",
headers=headers_a,
json={
"title": "住宅估价流程咨询",
"body": "请问住宅估价需要哪些材料?",
"attachmentFileIds": [file_id],
},
)
assert created.status_code == 200, created.text
question_id = created.json()["id"]
mine = client.get("/api/site/member/expert-questions", headers=headers_a)
assert mine.status_code == 200, mine.text
assert mine.json()["items"][0]["title"] == "住宅估价流程咨询"
assert mine.json()["items"][0]["status"] == "pending"
other = client.get(f"/api/site/member/expert-questions/{question_id}", headers=headers_b)
assert other.status_code == 404
other_list = client.get("/api/site/member/expert-questions", headers=headers_b)
assert other_list.json()["items"] == []
pending = client.get("/api/admin/expert-questions/pending-count", headers=staff)
assert pending.status_code == 200, pending.text
assert pending.json()["count"] >= 1
listed = client.get("/api/admin/expert-questions?status=pending", headers=staff)
assert listed.status_code == 200, listed.text
assert any(item["id"] == question_id for item in listed.json()["items"])
member_reply = client.post(
f"/api/admin/expert-questions/{question_id}/reply",
headers=headers_a,
json={"bodyHtml": "<p>会员不得回复</p>", "attachmentFileIds": []},
)
assert member_reply.status_code in (401, 403)
replied = client.post(
f"/api/admin/expert-questions/{question_id}/reply",
headers=staff,
json={
"bodyHtml": "<p>请准备产权证与房屋平面图</p><script>alert(1)</script>",
"attachmentFileIds": [],
},
)
assert replied.status_code == 200, replied.text
detail = client.get(f"/api/site/member/expert-questions/{question_id}", headers=headers_a)
assert detail.status_code == 200, detail.text
assert detail.json()["status"] == "replied"
assert "产权证" in detail.json()["reply"]["bodyHtml"]
assert "script" not in detail.json()["reply"]["bodyHtml"].lower()
assert detail.json()["attachments"][0]["filename"] == "证据.pdf"
assert "contentType" in detail.json()["attachments"][0]
after = client.get("/api/admin/expert-questions/pending-count", headers=staff)
assert after.json()["count"] == pending.json()["count"] - 1
# 人为把另一条提问改成超时
overdue_created = client.post(
"/api/site/member/expert-questions",
headers=headers_a,
json={"title": "超时样例", "body": "等待回复", "attachmentFileIds": []},
)
assert overdue_created.status_code == 200, overdue_created.text
overdue_id = overdue_created.json()["id"]
from app.db import SessionLocal
from app.models.expert import ExpertQuestion
db = SessionLocal()
try:
row = db.get(ExpertQuestion, overdue_id)
row.created_at = datetime.now() - timedelta(days=14)
db.commit()
finally:
db.close()
admin_detail = client.get(f"/api/admin/expert-questions/{overdue_id}", headers=staff)
assert admin_detail.status_code == 200, admin_detail.text
assert admin_detail.json()["overdue"] is True