"""§31 應用內回報 endpoint 測試。""" from __future__ import annotations import pytest from fastapi.testclient import TestClient from app.auth.passwords import hash_password from app.db import _utcnow, get_conn from app.main import app PWD = "TestPw123!" @pytest.fixture() def client(): return TestClient(app) def _mkuser(name, role="user"): c = get_conn() c.execute( "INSERT INTO users (username, email, password_hash, role, department, " "created_at, is_active) VALUES (?,?,?,?,?,?,1)", (name, f"{name}@x.tw", hash_password(PWD), role, "業務部", _utcnow()), ) return c.execute("SELECT id FROM users WHERE username=?", (name,)).fetchone()["id"] def _login(c, u): return c.post("/api/auth/login", json={"username": u, "password": PWD}).json()["access_token"] def _auth(t): return {"Authorization": f"Bearer {t}"} def test_create_feedback(client): _mkuser("u1") tok = _login(client, "u1") r = client.post("/api/feedback", headers=_auth(tok), json={ "category": "bug", "title": "登入頁卡住", "description": "點登入沒反應", "page_url": "/login", }) assert r.status_code == 201 assert r.json()["status"] == "open" def test_create_feedback_invalid_category(client): _mkuser("u1") tok = _login(client, "u1") r = client.post("/api/feedback", headers=_auth(tok), json={ "category": "spam", "title": "x", "description": "y", }) assert r.status_code == 422 def test_create_feedback_title_too_long(client): _mkuser("u1") tok = _login(client, "u1") r = client.post("/api/feedback", headers=_auth(tok), json={ "category": "bug", "title": "x" * 31, "description": "y", }) assert r.status_code == 422 def test_feedback_notifies_admin(client): aid = _mkuser("adm", "admin") _mkuser("u1") tok = _login(client, "u1") client.post("/api/feedback", headers=_auth(tok), json={ "category": "feature", "title": "想要暗色模式", "description": "晚上看會刺眼", }) rows = get_conn().execute( "SELECT type FROM notifications WHERE user_id=? AND type='feedback'", (aid,), ).fetchall() assert len(rows) >= 1 def test_admin_list_feedback(client): _mkuser("adm", "admin") uid = _mkuser("u1") tok_u = _login(client, "u1") client.post("/api/feedback", headers=_auth(tok_u), json={ "category": "bug", "title": "t1", "description": "d1", }) tok_a = _login(client, "adm") r = client.get("/api/admin/feedback", headers=_auth(tok_a)) assert r.status_code == 200 j = r.json() assert j["count"] >= 1 assert j["items"][0]["username"] == "u1" def test_admin_list_filter_status(client): _mkuser("adm", "admin") uid = _mkuser("u1") tok_u = _login(client, "u1") client.post("/api/feedback", headers=_auth(tok_u), json={ "category": "bug", "title": "t1", "description": "d1", }) tok_a = _login(client, "adm") r = client.get("/api/admin/feedback?status=open", headers=_auth(tok_a)) assert all(it["status"] == "open" for it in r.json()["items"]) def test_user_blocked_from_admin_list(client): _mkuser("u1") tok = _login(client, "u1") r = client.get("/api/admin/feedback", headers=_auth(tok)) assert r.status_code in (401, 403) def test_admin_reply_notifies_reporter(client): _mkuser("adm", "admin") uid = _mkuser("u1") tok_u = _login(client, "u1") fid = client.post("/api/feedback", headers=_auth(tok_u), json={ "category": "bug", "title": "t1", "description": "d1", }).json()["id"] tok_a = _login(client, "adm") r = client.patch(f"/api/admin/feedback/{fid}", headers=_auth(tok_a), json={ "status": "resolved", "admin_reply": "已修復,感謝回報", }) assert r.status_code == 200 # reporter 收到回覆通知 rows = get_conn().execute( "SELECT type FROM notifications WHERE user_id=? AND type='feedback_reply'", (uid,), ).fetchall() assert len(rows) >= 1 # status 更新 row = get_conn().execute("SELECT status, admin_reply FROM feedback WHERE id=?", (fid,)).fetchone() assert row["status"] == "resolved" assert "已修復" in row["admin_reply"] def test_admin_reply_404(client): _mkuser("adm", "admin") tok_a = _login(client, "adm") r = client.patch("/api/admin/feedback/99999", headers=_auth(tok_a), json={"status": "resolved"}) assert r.status_code == 404 def test_unauth_create_blocked(client): r = client.post("/api/feedback", json={"category": "bug", "title": "t", "description": "d"}) assert r.status_code in (401, 403) # ============ §50.3.2 文件報告錯誤 ============ def _mkdoc(title, author_id, dept="業務部"): c = get_conn() c.execute( "INSERT INTO documents (title, category, tags, summary, body, author_id, " "owner_department, visibility, allowed_departments, access_list, status, " "created_at, updated_at) VALUES (?,?,'[]','','x',?,?,'public','[]','[]','active',?,?)", (title, "知識/筆記", author_id, dept, _utcnow(), _utcnow()), ) return c.execute("SELECT id FROM documents WHERE title=?", (title,)).fetchone()["id"] def test_document_error_requires_doc_id(client): _mkuser("u1") tok = _login(client, "u1") r = client.post("/api/feedback", headers=_auth(tok), json={ "category": "document_error", "title": "內容過時", "description": "配方比例錯了", }) assert r.status_code == 400 assert r.json()["detail"]["error"] == "doc_id_required" def test_document_error_nonexistent_doc(client): _mkuser("u1") tok = _login(client, "u1") r = client.post("/api/feedback", headers=_auth(tok), json={ "category": "document_error", "title": "x", "description": "y", "doc_id": 999999, }) assert r.status_code == 404 def test_document_error_report_notifies_owner_and_count(client): owner_id = _mkuser("owner1") reporter_id = _mkuser("reporter1") other_id = _mkuser("other1") did = _mkdoc("甘油配方文件", owner_id) rtok = _login(client, "reporter1") r = client.post("/api/feedback", headers=_auth(rtok), json={ "category": "document_error", "title": "比例錯誤", "description": "甘油比例應為 8 成", "doc_id": did, "page_url": f"/documents/{did}", }) assert r.status_code == 201 # owner 收到通知 c = get_conn() n = c.execute( "SELECT COUNT(*) AS n FROM notifications WHERE user_id=? AND type='feedback'", (owner_id,) ).fetchone()["n"] assert n >= 1 # owner 看得到 error-count otok = _login(client, "owner1") rc = client.get(f"/api/feedback/document/{did}/error-count", headers=_auth(otok)) assert rc.status_code == 200 and rc.json()["open_count"] == 1 and rc.json()["visible"] is True # 無關 user 看不到(visible False, count 0) xtok = _login(client, "other1") rx = client.get(f"/api/feedback/document/{did}/error-count", headers=_auth(xtok)) assert rx.json()["visible"] is False and rx.json()["open_count"] == 0