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."