Files
PolyWeather/tests/test_user_feedback.py
2026-06-14 04:57:42 +08:00

340 lines
11 KiB
Python

from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from starlette.requests import Request
from src.database.db_manager import DBManager
import web.core as web_core
from web.services import feedback_api
from web.services import ops_api
def test_user_feedback_round_trip_includes_context_and_status(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback.db"))
created = db.append_user_feedback(
category="bug",
message="The Helsinki chart keeps loading.",
source="chart",
contact="pilot@example.com",
user_id="user-123",
user_email="pilot@example.com",
context={"city": "helsinki", "slot": 3, "detail_error": "timeout"},
)
assert created["id"] > 0
assert created["status"] == "open"
rows = db.list_user_feedback(limit=10)
assert len(rows) == 1
assert rows[0]["category"] == "bug"
assert rows[0]["message"] == "The Helsinki chart keeps loading."
assert rows[0]["source"] == "chart"
assert rows[0]["contact"] == "pilot@example.com"
assert rows[0]["user_id"] == "user-123"
assert rows[0]["user_email"] == "pilot@example.com"
assert rows[0]["context"]["city"] == "helsinki"
assert rows[0]["context"]["detail_error"] == "timeout"
updated = db.update_user_feedback_status(created["id"], status="triaged")
assert updated["status"] == "triaged"
assert db.list_user_feedback(limit=10, status="triaged")[0]["id"] == created["id"]
def test_user_feedback_status_filter_excludes_other_statuses(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback-filter.db"))
db.append_user_feedback(category="idea", message="Add a dark chart grid.")
closed = db.append_user_feedback(category="bug", message="Payment page failed.")
db.update_user_feedback_status(closed["id"], status="closed")
open_rows = db.list_user_feedback(limit=10, status="open")
assert [row["status"] for row in open_rows] == ["open"]
assert open_rows[0]["message"] == "Add a dark chart grid."
def test_user_feedback_reward_metadata_round_trip(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback-reward.db"))
created = db.append_user_feedback(
category="data",
message="Hong Kong COWIN reading was stale.",
user_id="user-reward",
user_email="reward@example.com",
)
assert created["reward_points"] == 0
assert created["reward_reason"] == ""
assert created["reward_status"] == ""
assert created["rewarded_at"] is None
rewarded = db.update_user_feedback_reward(
created["id"],
points=300,
reason="Valid data freshness report",
status="granted",
)
assert rewarded is not None
assert rewarded["reward_points"] == 300
assert rewarded["reward_reason"] == "Valid data freshness report"
assert rewarded["reward_status"] == "granted"
assert rewarded["rewarded_at"]
row = db.list_user_feedback(
limit=10,
user_id="user-reward",
user_email="reward@example.com",
)[0]
assert row["id"] == created["id"]
assert row["reward_points"] == 300
assert row["reward_reason"] == "Valid data freshness report"
assert row["reward_status"] == "granted"
assert row["rewarded_at"] == rewarded["rewarded_at"]
def test_feedback_reward_grant_adds_points_and_marks_feedback(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback-grant.db"))
db.upsert_user(1001, "pilot")
with db._get_connection() as conn: # noqa: SLF001
conn.execute(
"""
UPDATE users
SET points = ?, supabase_email = ?
WHERE telegram_id = ?
""",
(50, "pilot@example.com", 1001),
)
conn.commit()
created = db.append_user_feedback(
category="data",
message="Amsterdam METAR stale.",
user_id="user-1001",
user_email="pilot@example.com",
)
result = db.grant_feedback_reward(
created["id"],
points=300,
)
assert result["ok"] is True
assert result["points_before"] == 50
assert result["points_added"] == 300
assert result["points_after"] == 350
assert result["feedback"]["reward_points"] == 300
assert result["feedback"]["reward_reason"] == ""
assert result["feedback"]["reward_status"] == "granted"
assert db.get_points_by_supabase_email("pilot@example.com") == 350
duplicate = db.grant_feedback_reward(
created["id"],
points=300,
)
assert duplicate["ok"] is False
assert duplicate["reason"] == "already_rewarded"
assert duplicate["points_after"] == 350
def test_ops_feedback_reward_service_returns_operator(monkeypatch):
class FakeDB:
def grant_feedback_reward(self, feedback_id, *, points, reason=""):
return {
"ok": True,
"feedback_id": feedback_id,
"points_added": points,
"points_after": 900,
"feedback": {
"id": feedback_id,
"reward_points": points,
"reward_reason": reason,
"reward_status": "granted",
},
}
monkeypatch.setattr(ops_api, "_require_ops", lambda request: {"email": "ops@example.com"})
monkeypatch.setattr(ops_api, "DBManager", lambda: FakeDB())
payload = ops_api.grant_ops_feedback_reward(
object(),
feedback_id=42,
points=500,
)
assert payload["ok"] is True
assert payload["operator_email"] == "ops@example.com"
assert payload["feedback"]["reward_points"] == 500
assert payload["feedback"]["reward_reason"] == ""
def test_ops_feedback_reward_falls_back_to_supabase_user_without_telegram_binding(monkeypatch):
updated_feedback = {
"id": 19,
"user_id": "supabase-user-19",
"user_email": "927794117@qq.com",
"reward_points": 500,
"reward_reason": "",
"reward_status": "granted",
}
class FakeDB:
def grant_feedback_reward(self, feedback_id, *, points, reason=""):
return {
"ok": False,
"reason": "user_not_found",
"feedback": {
"id": feedback_id,
"user_id": "supabase-user-19",
"user_email": "927794117@qq.com",
},
}
def update_user_feedback_reward(self, feedback_id, *, points, reason="", status="granted"):
assert feedback_id == 19
assert points == 500
assert reason == ""
assert status == "granted"
return updated_feedback
class FakeEntitlement:
def grant_points_to_user(self, user_id, points):
assert user_id == "supabase-user-19"
assert points == 500
return {
"ok": True,
"source": "supabase_metadata",
"points_before": 100,
"points_added": 500,
"points_after": 600,
}
monkeypatch.setattr(ops_api, "_require_ops", lambda request: {"email": "ops@example.com"})
monkeypatch.setattr(ops_api, "DBManager", lambda: FakeDB())
monkeypatch.setattr(ops_api.legacy_routes, "SUPABASE_ENTITLEMENT", FakeEntitlement())
payload = ops_api.grant_ops_feedback_reward(
object(),
feedback_id=19,
points=500,
)
assert payload["ok"] is True
assert payload["source"] == "supabase_metadata"
assert payload["points_after"] == 600
assert payload["feedback"] == updated_feedback
assert payload["operator_email"] == "ops@example.com"
def test_user_feedback_identity_filter_returns_only_matching_user(tmp_path):
db = DBManager(str(tmp_path / "polyweather-feedback-identity.db"))
mine_by_user_id = db.append_user_feedback(
category="bug",
message="My chart needs attention.",
user_id="user-a",
user_email="a@example.com",
)
mine_by_email = db.append_user_feedback(
category="data",
message="My older email-only report.",
user_email="a@example.com",
)
db.append_user_feedback(
category="bug",
message="Someone else's report.",
user_id="user-b",
user_email="b@example.com",
)
db.append_user_feedback(
category="idea",
message="Anonymous report should not leak.",
contact="a@example.com",
)
rows = db.list_user_feedback(
limit=10,
user_id="user-a",
user_email="a@example.com",
)
assert [row["id"] for row in rows] == [mine_by_email["id"], mine_by_user_id["id"]]
assert {row["message"] for row in rows} == {
"My chart needs attention.",
"My older email-only report.",
}
def test_list_current_user_feedback_requires_identity(tmp_path, monkeypatch):
db = DBManager(str(tmp_path / "polyweather-feedback-current-user.db"))
db.append_user_feedback(
category="bug",
message="Mine.",
user_id="user-a",
user_email="a@example.com",
)
db.append_user_feedback(
category="bug",
message="Not mine.",
user_id="user-b",
user_email="b@example.com",
)
monkeypatch.setattr(feedback_api, "DBManager", lambda: db)
monkeypatch.setattr(feedback_api.legacy_routes, "_bind_optional_supabase_identity", lambda request: None)
request = SimpleNamespace(
state=SimpleNamespace(auth_user_id="user-a", auth_email="a@example.com")
)
payload = feedback_api.list_current_user_feedback(request, limit=20)
assert payload["total"] == 1
assert payload["feedback"][0]["message"] == "Mine."
anonymous_request = SimpleNamespace(state=SimpleNamespace())
with pytest.raises(HTTPException) as exc_info:
feedback_api.list_current_user_feedback(anonymous_request, limit=20)
assert exc_info.value.status_code == 401
def test_list_current_user_feedback_accepts_forwarded_identity_when_supabase_unconfigured(
tmp_path, monkeypatch
):
db = DBManager(str(tmp_path / "polyweather-feedback-forwarded.db"))
db.append_user_feedback(
category="bug",
message="Forwarded identity feedback.",
user_id="user-a",
user_email="a@example.com",
)
db.append_user_feedback(
category="bug",
message="Other user feedback.",
user_id="user-b",
user_email="b@example.com",
)
monkeypatch.setattr(feedback_api, "DBManager", lambda: db)
monkeypatch.setattr(web_core, "_ENTITLEMENT_TOKEN", "backend-token")
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "")
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "")
request = Request(
{
"type": "http",
"headers": [
(b"x-polyweather-entitlement", b"backend-token"),
(b"x-polyweather-auth-user-id", b"user-a"),
(b"x-polyweather-auth-email", b"a@example.com"),
(b"authorization", b"Bearer access-token"),
],
}
)
payload = feedback_api.list_current_user_feedback(request, limit=20)
assert payload["total"] == 1
assert payload["feedback"][0]["message"] == "Forwarded identity feedback."