shinkan-jinkendo/backend/tests/test_p13_content_reports.py

345 lines
13 KiB
Python
Raw Normal View History

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