"""行业自律:三类栏目文章公开阅读;投诉加密保存,查询码只看状态。""" 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": "
内部草稿
", "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": "已脱敏的处理结果
", "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": "受理估价执业违规
", "processHtml": "受理后核查
", }, ) 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}, )