""" P-13: Content-Melde-Backend – Backend-Tests. Abgedeckt (15 Faelle): 1. submit_report ohne good_faith_confirmed=False → 400 2. submit_report mit leerem report_description → 422 3. submit_report mit fehlender reporter_email → 422 4. submit_report ohne Login fuer official-Medium – Erfolgspfad 5. submit_report ohne Login fuer nicht-official-Medium → 404 6. submit_report mit Login fuer sichtbares club-Medium – Erfolgspfad 7. submit_report mit Login fuer nicht-sichtbares fremdes Medium → 404 8. Prioritaet high fuer illegal_content/minors/youth_protection 9. Prioritaet normal fuer copyright/other 10. list_inbox als Nicht-Admin → 403 11. list_inbox als Plattform-Admin – gibt Liste zurueck 12. patch_report status 'under_review' – Erfolgspfad 13. patch_report 'resolved_no_action' ohne resolution_note → 400 14. set_legal_hold_from_report als Nicht-Superadmin → 403 15. set_legal_hold_from_report als Superadmin – Erfolgspfad (reason_code-Mapping) """ from __future__ import annotations import os from unittest.mock import MagicMock, patch import pytest from fastapi import HTTPException os.environ.setdefault("SKIP_DB_MIGRATE", "1") from routers.content_reports import ( HIGH_PRIORITY_REASONS, _REASON_TO_HOLD_CODE, _is_media_asset_visible_anonymous, _is_media_asset_visible_to_profile, REPORT_REASONS, ContentReportCreate, ContentReportPatch, ) # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- def _make_cur(fetchone_val=None): cur = MagicMock() cur.fetchone.return_value = fetchone_val cur.fetchall.return_value = [] return cur def _row(data: dict): """Simuliert eine psycopg2-DictRow.""" m = MagicMock() m.__getitem__ = lambda self, k: data[k] m.keys = lambda: data.keys() return m # --------------------------------------------------------------------------- # 1. good_faith_confirmed=False → 400 # --------------------------------------------------------------------------- def test_submit_requires_good_faith(): body = ContentReportCreate( target_type="media_asset", target_id=1, report_reason="copyright", report_description="A" * 20, reporter_name="Max", reporter_email="max@example.com", good_faith_confirmed=False, ) from routers.content_reports import submit_content_report with pytest.raises(HTTPException) as exc: submit_content_report(body) assert exc.value.status_code == 400 assert "Gutglauben" in exc.value.detail # --------------------------------------------------------------------------- # 2. Leere report_description → 422 (Pydantic) # --------------------------------------------------------------------------- def test_submit_requires_description(): with pytest.raises(Exception): ContentReportCreate( target_type="media_asset", target_id=1, report_reason="copyright", report_description="ab", # unter min_length=10 reporter_name="Max", reporter_email="max@example.com", good_faith_confirmed=True, ) # --------------------------------------------------------------------------- # 3. Ungueltige E-Mail → 422 (Pydantic Validator) # --------------------------------------------------------------------------- def test_submit_requires_valid_email(): with pytest.raises(Exception): ContentReportCreate( target_type="media_asset", target_id=1, report_reason="copyright", report_description="A" * 20, reporter_name="Max", reporter_email="kein-at-zeichen", good_faith_confirmed=True, ) # --------------------------------------------------------------------------- # 4. Anonym – official-Medium → sichtbar # --------------------------------------------------------------------------- def test_anonymous_official_medium_visible(): cur = _make_cur(fetchone_val={"1": 1}) cur.fetchone.return_value = MagicMock() result = _is_media_asset_visible_anonymous(cur, asset_id=42) assert result is True # --------------------------------------------------------------------------- # 5. Anonym – nicht-official-Medium → nicht sichtbar # --------------------------------------------------------------------------- def test_anonymous_non_official_medium_not_visible(): cur = _make_cur(fetchone_val=None) result = _is_media_asset_visible_anonymous(cur, asset_id=99) assert result is False # --------------------------------------------------------------------------- # 6. Eingeloggter Nutzer – sichtbares club-Medium → True # --------------------------------------------------------------------------- def test_logged_in_club_member_can_see_club_media(): asset_row = MagicMock() asset_row.keys = lambda: ["id", "visibility", "club_id", "uploaded_by_profile_id"] asset_row.__getitem__ = lambda s, k: { "id": 7, "visibility": "club", "club_id": 3, "uploaded_by_profile_id": 99, }[k] member_row = MagicMock() # Mitgliedschaft gefunden cur = MagicMock() cur.fetchone.side_effect = [asset_row, member_row] result = _is_media_asset_visible_to_profile(cur, asset_id=7, profile_id=1, global_role="trainer") assert result is True # --------------------------------------------------------------------------- # 7. Eingeloggter Nutzer – fremdes Medium → False # --------------------------------------------------------------------------- def test_logged_in_non_member_cannot_see_club_media(): asset_row = MagicMock() asset_row.keys = lambda: ["id", "visibility", "club_id", "uploaded_by_profile_id"] asset_row.__getitem__ = lambda s, k: { "id": 7, "visibility": "club", "club_id": 3, "uploaded_by_profile_id": 99, }[k] cur = MagicMock() # Erster fetchone: Asset-Zeile, zweiter: kein Mitglied cur.fetchone.side_effect = [asset_row, None] result = _is_media_asset_visible_to_profile(cur, asset_id=7, profile_id=5, global_role="trainer") assert result is False # --------------------------------------------------------------------------- # 8. Prioritaet high fuer kritische Gruende # --------------------------------------------------------------------------- def test_high_priority_reasons(): assert "illegal_content" in HIGH_PRIORITY_REASONS assert "minors" in HIGH_PRIORITY_REASONS assert "youth_protection" in HIGH_PRIORITY_REASONS # --------------------------------------------------------------------------- # 9. Prioritaet normal fuer andere Gruende # --------------------------------------------------------------------------- def test_normal_priority_reasons(): normal_reasons = REPORT_REASONS - HIGH_PRIORITY_REASONS assert "copyright" in normal_reasons assert "other" in normal_reasons assert "privacy" in normal_reasons # --------------------------------------------------------------------------- # 10. list_inbox als Nicht-Admin → 403 # --------------------------------------------------------------------------- def test_list_inbox_requires_platform_admin(): from routers.content_reports import list_inbox_content_reports tenant = MagicMock() tenant.global_role = "trainer" tenant.profile_id = 99 # COUNT-Abfrage fuer Club-Admin-Rollen → 0 → 403 cnt_row = _row({"cnt": 0}) mock_cur = MagicMock() mock_cur.fetchone.return_value = cnt_row mock_conn = MagicMock() mock_conn_ctx = MagicMock() mock_conn_ctx.__enter__ = MagicMock(return_value=mock_conn) mock_conn_ctx.__exit__ = MagicMock(return_value=False) with patch("routers.content_reports.get_db", return_value=mock_conn_ctx), \ patch("routers.content_reports.get_cursor", return_value=mock_cur): with pytest.raises(HTTPException) as exc: list_inbox_content_reports(tenant=tenant) assert exc.value.status_code == 403 # --------------------------------------------------------------------------- # 11. list_inbox als Plattform-Admin – gibt Liste zurueck # --------------------------------------------------------------------------- def test_list_inbox_as_admin_returns_list(): from routers.content_reports import list_inbox_content_reports tenant = MagicMock() tenant.global_role = "admin" mock_conn = MagicMock() mock_cur = MagicMock() mock_cur.fetchall.return_value = [] mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn_ctx = MagicMock() mock_conn_ctx.__enter__ = MagicMock(return_value=mock_conn) mock_conn_ctx.__exit__ = MagicMock(return_value=False) with patch("routers.content_reports.get_db", return_value=mock_conn_ctx), \ patch("routers.content_reports.get_cursor", return_value=mock_cur): result = list_inbox_content_reports(tenant=tenant) assert isinstance(result, list) # --------------------------------------------------------------------------- # 12. patch_report status 'under_review' – Erfolgspfad # --------------------------------------------------------------------------- def test_patch_report_under_review(): from routers.content_reports import patch_content_report tenant = MagicMock() tenant.global_role = "admin" tenant.profile_id = 1 body = ContentReportPatch(status="under_review") existing_row = MagicMock() existing_row.__getitem__ = lambda s, k: { "id": 5, "status": "submitted", "target_type": "media_asset", "target_id": 42, "resolution_note": None, }[k] existing_row.keys = lambda: ["id", "status", "target_type", "target_id", "resolution_note"] updated_row = MagicMock() updated_row.__getitem__ = lambda s, k: {"id": 5, "status": "under_review"}[k] updated_row.keys = lambda: ["id", "status"] mock_cur = MagicMock() mock_cur.fetchone.side_effect = [existing_row, updated_row] mock_conn = MagicMock() mock_conn_ctx = MagicMock() mock_conn_ctx.__enter__ = MagicMock(return_value=mock_conn) mock_conn_ctx.__exit__ = MagicMock(return_value=False) with patch("routers.content_reports.get_db", return_value=mock_conn_ctx), \ patch("routers.content_reports.get_cursor", return_value=mock_cur): result = patch_content_report(5, body, tenant=tenant) assert result["ok"] is True assert result["status"] == "under_review" # --------------------------------------------------------------------------- # 13. patch_report 'resolved_no_action' ohne resolution_note → 400 # --------------------------------------------------------------------------- def test_patch_resolved_no_action_requires_note(): from routers.content_reports import patch_content_report tenant = MagicMock() tenant.global_role = "admin" tenant.profile_id = 1 body = ContentReportPatch(status="resolved_no_action", resolution_note=None) with pytest.raises(HTTPException) as exc: patch_content_report(99, body, tenant=tenant) assert exc.value.status_code == 400 assert "Begründung" in exc.value.detail # --------------------------------------------------------------------------- # 14. set_legal_hold_from_report als Nicht-Superadmin → 403 # --------------------------------------------------------------------------- def test_legal_hold_from_report_requires_superadmin(): from routers.content_reports import set_legal_hold_from_report, ContentReportLegalHoldBody tenant = MagicMock() tenant.global_role = "admin" # nur admin, nicht superadmin body = ContentReportLegalHoldBody(reason_note="Test-Begruendung") with pytest.raises(HTTPException) as exc: set_legal_hold_from_report(1, body, tenant=tenant) assert exc.value.status_code == 403 # --------------------------------------------------------------------------- # 15. Reason-Code-Mapping: copyright → copyright_complaint # --------------------------------------------------------------------------- def test_reason_code_mapping(): assert _REASON_TO_HOLD_CODE["copyright"] == "copyright_complaint" assert _REASON_TO_HOLD_CODE["minors"] == "youth_protection" assert _REASON_TO_HOLD_CODE["illegal_content"] == "illegal_content" assert _REASON_TO_HOLD_CODE["privacy"] == "privacy_complaint" assert _REASON_TO_HOLD_CODE["image_rights"] == "rights_dispute" assert _REASON_TO_HOLD_CODE["other"] == "other" # Alle Reason-Codes muessen gemappt sein for reason in REPORT_REASONS: assert reason in _REASON_TO_HOLD_CODE, f"{reason} fehlt im Mapping"