Files
gsfdc/server/tests/test_discipline.py

320 lines
12 KiB
Python

"""行业自律:三类栏目文章公开阅读;投诉加密保存,查询码只看状态。"""
import base64
import hashlib
from datetime import datetime, timedelta
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from app.core.config import get_settings
from app.core.security import create_access_token
@pytest.fixture
def client(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "test-secret-must-be-32-characters!")
monkeypatch.setenv("API_PAYLOAD_ENCRYPT", "false")
key = base64.b64encode(hashlib.sha256(b"discipline-field-key").digest()).decode("ascii")
monkeypatch.setenv("FIELD_AES_KEY", key)
get_settings.cache_clear()
if not get_settings().database_url.strip():
pytest.skip("DATABASE_URL 为空")
from app.main import create_app
with TestClient(create_app()) as api:
yield api
get_settings.cache_clear()
def _headers(token_type: str) -> dict[str, str]:
return {"Authorization": f"Bearer {create_access_token('1', token_type)}"}
def _minio_ready() -> bool:
try:
from app.services.storage import ensure_buckets
ensure_buckets()
return True
except Exception:
return False
def _png() -> bytes:
return (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f"
b"\x00\x00\x01\x01\x00\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82"
)
def _clear_category(client, staff: dict[str, str], category: str) -> None:
listed = client.get(f"/api/admin/about/articles/{category}", headers=staff)
assert listed.status_code == 200, listed.text
for item in listed.json().get("items") or []:
deleted = client.delete(f"/api/admin/home/articles/{item['id']}", headers=staff)
assert deleted.status_code in (200, 204), deleted.text
def _purge_complaints(codes: list[str]) -> list[int]:
from app.db import SessionLocal
from app.models.discipline import DisciplineComplaint, DisciplineComplaintFile
db = SessionLocal()
file_ids: list[int] = []
try:
rows = db.scalars(select(DisciplineComplaint).where(DisciplineComplaint.code.in_(codes))).all()
ids = [row.id for row in rows]
if not ids:
return []
links = db.scalars(
select(DisciplineComplaintFile).where(DisciplineComplaintFile.complaint_id.in_(ids))
).all()
file_ids = [link.file_id for link in links]
for link in links:
db.delete(link)
for row in rows:
db.delete(row)
db.commit()
finally:
db.close()
return file_ids
def test_discipline_articles_notice_and_complaint(client):
if not _minio_ready():
pytest.skip("MinIO 不可达")
staff = _headers("staff")
member = _headers("member")
article_ids: list[int] = []
file_ids: list[int] = []
codes: list[str] = []
word_id = None
previous_workdays = None
try:
for category, placeholder in (
("warning_cases", "暂无警示案例"),
("integrity_records", "暂无诚信档案"),
("discipline_results", "暂无结果公示"),
):
_clear_category(client, staff, category)
empty = client.get(f"/api/site/about/articles/{category}")
assert empty.status_code == 200, empty.text
assert placeholder in empty.text
denied = client.get("/api/admin/discipline/complaints", headers=member)
assert denied.status_code == 403, denied.text
assert client.get("/api/admin/discipline/complaints").status_code == 401
cover = client.post(
"/api/admin/files",
headers=staff,
data={"bucket": "public"},
files={"file": ("cover.png", _png(), "image/png")},
)
assert cover.status_code == 200, cover.text
file_ids.append(cover.json()["id"])
attachment = client.post(
"/api/admin/files",
headers=staff,
data={"bucket": "public"},
files={"file": ("note.txt", b"public-note", "text/plain")},
)
assert attachment.status_code == 200, attachment.text
file_ids.append(attachment.json()["id"])
draft = client.post(
"/api/admin/home/articles",
headers=staff,
json={
"category": "warning_cases",
"title": "未发布警示",
"published": False,
"bodyHtml": "<p>内部草稿</p>",
"attachmentFileIds": [],
},
)
assert draft.status_code == 200, draft.text
article_ids.append(draft.json()["id"])
published = client.post(
"/api/admin/home/articles",
headers=staff,
json={
"category": "discipline_results",
"title": "某机构处理结果",
"published": True,
"bodyHtml": "<p>已脱敏的处理结果</p><script>alert(1)</script>",
"imageFileId": file_ids[0],
"attachmentFileIds": [file_ids[1]],
},
)
assert published.status_code == 200, published.text
article_ids.append(published.json()["id"])
hidden = client.get("/api/site/about/articles/warning_cases")
assert "未发布警示" not in hidden.text
shown = client.get("/api/site/about/articles/discipline_results")
titles = [item["title"] for item in shown.json()["items"]]
assert "某机构处理结果" in titles
detail = client.get(f"/api/site/home/articles/{published.json()['id']}")
assert "已脱敏的处理结果" in detail.text
assert "script" not in detail.text
assert "note.txt" in detail.text
home = client.get("/api/admin/home", headers=staff)
categories = {item["category"] for item in home.json()["articles"]}
assert "warning_cases" not in categories
assert "discipline_results" not in categories
settings = client.get("/api/admin/site-settings", headers=staff)
assert settings.status_code == 200, settings.text
previous_workdays = settings.json().get("complaint_workdays", "")
updated = client.put(
"/api/admin/site-settings",
headers=staff,
json={"complaint_workdays": 12},
)
assert updated.status_code == 200, updated.text
notice = client.put(
"/api/admin/discipline/notice",
headers=staff,
json={
"phone": "0931-1234567",
"email": "report@example.com",
"scopeHtml": "<p>受理估价执业违规</p><script>alert(1)</script>",
"processHtml": "<p>受理后核查</p>",
},
)
assert notice.status_code == 200, notice.text
assert notice.json()["workdays"] == 12
public_notice = client.get("/api/site/discipline/notice")
assert public_notice.status_code == 200, public_notice.text
assert "0931-1234567" in public_notice.text
assert "受理估价执业违规" in public_notice.text
assert "script" not in public_notice.text
assert public_notice.json()["workdays"] == 12
missing = client.post(
"/api/site/discipline/complaints",
data={
"title": "缺证据",
"subject": "某机构",
"body": "没有附件",
"contactName": "李四",
"mobile": "13800138000",
},
)
assert missing.status_code == 400, missing.text
assert "证据" in missing.text
word = "自律禁词" + hashlib.sha256(b"discipline-word").hexdigest()[:8]
created_word = client.post("/api/admin/sensitive-words", headers=staff, json={"word": word})
assert created_word.status_code == 200, created_word.text
word_id = created_word.json()["id"]
blocked = client.post(
"/api/site/discipline/complaints",
data={
"title": f"标题含{word}",
"subject": "某机构",
"body": "事实说明",
"contactName": "李四",
"mobile": "13800138000",
},
files=[("evidence", ("proof.png", _png(), "image/png"))],
)
assert blocked.status_code == 400, blocked.text
assert "敏感词" in blocked.text
secret = "只应工作人员看到的违规事实"
submitted = client.post(
"/api/site/discipline/complaints",
data={
"title": "估价报告违规",
"subject": "某估价机构",
"body": secret,
"contactName": "张三",
"mobile": "138-0013-8000",
"email": "a@example.com",
},
files=[("evidence", ("proof.png", _png(), "image/png"))],
)
assert submitted.status_code == 200, submitted.text
payload = submitted.json()
codes.append(payload["code"])
assert payload["status"] == "pending"
assert payload["workdays"] == 12
assert secret not in submitted.text
assert "13800138000" not in submitted.text
listed = client.get("/api/admin/discipline/complaints", headers=staff)
assert listed.status_code == 200, listed.text
item = next(row for row in listed.json()["items"] if row["code"] == payload["code"])
assert item["mobileMask"] == "138****8000"
assert item["emailMask"] == "a***@example.com"
assert secret not in listed.text
assert "13800138000" not in listed.text
assert item["attachmentCount"] == 1
opened = client.get(f"/api/admin/discipline/complaints/{item['id']}", headers=staff)
assert opened.status_code == 200, opened.text
assert opened.json()["mobile"] == "13800138000"
assert opened.json()["email"] == "a@example.com"
assert secret in opened.text
assert opened.json()["attachments"][0]["filename"] == "proof.png"
assert opened.json()["attachments"][0]["url"]
file_ids.append(opened.json()["attachments"][0]["id"])
status = client.get(f"/api/site/discipline/complaints/{payload['code']}")
assert status.status_code == 200, status.text
assert status.json()["statusLabel"] == "待处理"
assert secret not in status.text
assert "13800138000" not in status.text
assert "张三" not in status.text
assert "proof.png" not in status.text
from app.db import SessionLocal
from app.models.discipline import DisciplineComplaint
db = SessionLocal()
try:
row = db.scalar(select(DisciplineComplaint).where(DisciplineComplaint.code == payload["code"]))
assert row is not None
row.created_at = datetime.now() - timedelta(days=40)
db.commit()
finally:
db.close()
overdue = client.get("/api/admin/discipline/complaints?status=pending", headers=staff)
overdue_item = next(row for row in overdue.json()["items"] if row["code"] == payload["code"])
assert overdue_item["overdue"] is True
public_overdue = client.get(f"/api/site/discipline/complaints/{payload['code']}")
assert public_overdue.json()["overdue"] is True
done = client.post(f"/api/admin/discipline/complaints/{item['id']}/process", headers=staff)
assert done.status_code == 200, done.text
finished = client.get(f"/api/site/discipline/complaints/{payload['code'].lower()}")
assert finished.json()["status"] == "processed"
assert finished.json()["overdue"] is False
assert client.get("/api/site/discipline/complaints/NO-SUCH").status_code == 404
finally:
for article_id in article_ids:
client.delete(f"/api/admin/home/articles/{article_id}", headers=staff)
evidence_ids = _purge_complaints(codes)
for file_id in [*file_ids, *evidence_ids]:
client.delete(f"/api/admin/files/{file_id}", headers=staff)
if word_id is not None:
client.delete(f"/api/admin/sensitive-words/{word_id}", headers=staff)
client.put(
"/api/admin/discipline/notice",
headers=staff,
json={"phone": "", "email": "", "scopeHtml": "", "processHtml": ""},
)
if previous_workdays is not None:
client.put(
"/api/admin/site-settings",
headers=staff,
json={"complaint_workdays": previous_workdays},
)