| """§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 |
| |
| rows = get_conn().execute( |
| "SELECT type FROM notifications WHERE user_id=? AND type='feedback_reply'", (uid,), |
| ).fetchall() |
| assert len(rows) >= 1 |
| |
| 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) |
|
|
|
|
| |
|
|
| 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 |
| |
| 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 |
| |
| 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 |
| |
| 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 |
|
|