"""Tests for the feedback API backend.""" import pytest from unittest.mock import patch, MagicMock from pathlib import Path # ── mask_pii ────────────────────────────────────────────────────────────────── def test_mask_pii_email(): from scripts.feedback_api import mask_pii assert mask_pii("contact foo@bar.com please") == "contact [email redacted] please" def test_mask_pii_phone_dashes(): from scripts.feedback_api import mask_pii assert mask_pii("call 555-123-4567 now") == "call [phone redacted] now" def test_mask_pii_phone_parens(): from scripts.feedback_api import mask_pii assert mask_pii("(555) 867-5309") == "[phone redacted]" def test_mask_pii_clean_text(): from scripts.feedback_api import mask_pii assert mask_pii("no sensitive data here") == "no sensitive data here" def test_mask_pii_multiple_emails(): from scripts.feedback_api import mask_pii result = mask_pii("a@b.com and c@d.com") assert result == "[email redacted] and [email redacted]" # ── collect_context ─────────────────────────────────────────────────────────── def test_collect_context_required_keys(): from scripts.feedback_api import collect_context ctx = collect_context("Home") for key in ("page", "version", "tier", "llm_backend", "os", "timestamp"): assert key in ctx, f"missing key: {key}" def test_collect_context_page_value(): from scripts.feedback_api import collect_context ctx = collect_context("MyPage") assert ctx["page"] == "MyPage" def test_collect_context_timestamp_is_utc(): from scripts.feedback_api import collect_context ctx = collect_context("X") assert ctx["timestamp"].endswith("Z") # ── collect_logs ────────────────────────────────────────────────────────────── def test_collect_logs_returns_string(tmp_path): from scripts.feedback_api import collect_logs log = tmp_path / ".streamlit.log" log.write_text("line1\nline2\nline3\n") result = collect_logs(log_path=log, n=10) assert isinstance(result, str) assert "line3" in result def test_collect_logs_tails_n_lines(tmp_path): from scripts.feedback_api import collect_logs log = tmp_path / ".streamlit.log" log.write_text("\n".join(f"line{i}" for i in range(200))) result = collect_logs(log_path=log, n=10) assert "line199" in result assert "line0" not in result def test_collect_logs_masks_pii(tmp_path): from scripts.feedback_api import collect_logs log = tmp_path / "test.log" log.write_text("user foo@bar.com connected\n") result = collect_logs(log_path=log) assert "foo@bar.com" not in result assert "[email redacted]" in result def test_collect_logs_missing_file(tmp_path): from scripts.feedback_api import collect_logs result = collect_logs(log_path=tmp_path / "nonexistent.log") assert "no log file" in result.lower() # ── collect_listings ────────────────────────────────────────────────────────── def test_collect_listings_safe_fields_only(tmp_path): """Only title, company, url — no cover letters, notes, or emails.""" from scripts.db import init_db, insert_job from scripts.feedback_api import collect_listings db = tmp_path / "test.db" init_db(db) insert_job(db, { "title": "CSM", "company": "Acme", "url": "https://example.com/1", "source": "linkedin", "location": "Remote", "is_remote": True, "salary": "", "description": "great role", "date_found": "2026-03-01", }) results = collect_listings(db_path=db, n=5) assert len(results) == 1 assert set(results[0].keys()) == {"title", "company", "url"} assert results[0]["title"] == "CSM" def test_collect_listings_respects_n(tmp_path): from scripts.db import init_db, insert_job from scripts.feedback_api import collect_listings db = tmp_path / "test.db" init_db(db) for i in range(10): insert_job(db, { "title": f"Job {i}", "company": "Acme", "url": f"https://example.com/{i}", "source": "linkedin", "location": "Remote", "is_remote": False, "salary": "", "description": "", "date_found": "2026-03-01", }) assert len(collect_listings(db_path=db, n=3)) == 3 # ── build_issue_body ────────────────────────────────────────────────────────── def test_build_issue_body_contains_description(): from scripts.feedback_api import build_issue_body form = {"type": "bug", "title": "Test", "description": "it broke", "repro": ""} ctx = {"page": "Home", "version": "v1.0", "tier": "free", "llm_backend": "ollama", "os": "Linux", "timestamp": "2026-03-03T00:00:00Z"} body = build_issue_body(form, ctx, {}) assert "it broke" in body assert "Home" in body assert "v1.0" in body def test_build_issue_body_bug_includes_repro(): from scripts.feedback_api import build_issue_body form = {"type": "bug", "title": "X", "description": "desc", "repro": "step 1\nstep 2"} body = build_issue_body(form, {}, {}) assert "step 1" in body assert "Reproduction" in body def test_build_issue_body_no_repro_for_feature(): from scripts.feedback_api import build_issue_body form = {"type": "feature", "title": "X", "description": "add dark mode", "repro": "ignored"} body = build_issue_body(form, {}, {}) assert "Reproduction" not in body def test_build_issue_body_logs_in_collapsible(): from scripts.feedback_api import build_issue_body form = {"type": "other", "title": "X", "description": "Y", "repro": ""} body = build_issue_body(form, {}, {"logs": "log line 1\nlog line 2"}) assert "
" in body assert "log line 1" in body def test_build_issue_body_omits_logs_when_not_provided(): from scripts.feedback_api import build_issue_body form = {"type": "bug", "title": "X", "description": "Y", "repro": ""} body = build_issue_body(form, {}, {}) assert "
" not in body def test_build_issue_body_submitter_attribution(): from scripts.feedback_api import build_issue_body form = {"type": "bug", "title": "X", "description": "Y", "repro": ""} body = build_issue_body(form, {}, {"submitter": "Jane Doe "}) assert "Jane Doe" in body def test_build_issue_body_listings_shown(): from scripts.feedback_api import build_issue_body form = {"type": "bug", "title": "X", "description": "Y", "repro": ""} listings = [{"title": "CSM", "company": "Acme", "url": "https://example.com/1"}] body = build_issue_body(form, {}, {"listings": listings}) assert "CSM" in body assert "Acme" in body # ── Forgejo API ─────────────────────────────────────────────────────────────── @patch("scripts.feedback_api.requests.get") @patch("scripts.feedback_api.requests.post") def test_ensure_labels_uses_existing(mock_post, mock_get): from scripts.feedback_api import _ensure_labels mock_get.return_value.ok = True mock_get.return_value.json.return_value = [ {"name": "beta-feedback", "id": 1}, {"name": "bug", "id": 2}, ] ids = _ensure_labels( ["beta-feedback", "bug"], "https://example.com/api/v1", {"Authorization": "token x"}, "owner/repo" ) assert ids == [1, 2] mock_post.assert_not_called() @patch("scripts.feedback_api.requests.get") @patch("scripts.feedback_api.requests.post") def test_ensure_labels_creates_missing(mock_post, mock_get): from scripts.feedback_api import _ensure_labels mock_get.return_value.ok = True mock_get.return_value.json.return_value = [] mock_post.return_value.ok = True mock_post.return_value.json.return_value = {"id": 99} ids = _ensure_labels( ["needs-triage"], "https://example.com/api/v1", {"Authorization": "token x"}, "owner/repo" ) assert 99 in ids @patch("scripts.feedback_api._ensure_labels", return_value=[1, 2]) @patch("scripts.feedback_api.requests.post") def test_create_forgejo_issue_success(mock_post, mock_labels, monkeypatch): from scripts.feedback_api import create_forgejo_issue monkeypatch.setenv("FORGEJO_API_TOKEN", "testtoken") monkeypatch.setenv("FORGEJO_REPO", "owner/repo") monkeypatch.setenv("FORGEJO_API_URL", "https://example.com/api/v1") mock_post.return_value.status_code = 201 mock_post.return_value.raise_for_status = lambda: None mock_post.return_value.json.return_value = {"number": 42, "html_url": "https://example.com/issues/42"} result = create_forgejo_issue("Test issue", "body text", ["beta-feedback", "bug"]) assert result["number"] == 42 assert "42" in result["url"] @patch("scripts.feedback_api.requests.post") def test_upload_attachment_returns_url(mock_post, monkeypatch): from scripts.feedback_api import upload_attachment monkeypatch.setenv("FORGEJO_API_TOKEN", "testtoken") monkeypatch.setenv("FORGEJO_REPO", "owner/repo") monkeypatch.setenv("FORGEJO_API_URL", "https://example.com/api/v1") mock_post.return_value.status_code = 201 mock_post.return_value.raise_for_status = lambda: None mock_post.return_value.json.return_value = { "uuid": "abc", "browser_download_url": "https://example.com/assets/abc" } url = upload_attachment(42, b"\x89PNG", "screenshot.png") assert url == "https://example.com/assets/abc"