shinkan-jinkendo/backend/tests/test_p13_content_reports.py
Lars 60709df615
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 58s
feat: Implement Content Reporting Backend
- Added new API endpoints for content reporting, including submission, retrieval, and status updates.
- Created database migration for `content_reports` table to store report data.
- Integrated content reports into the existing admin inbox for better management.
- Implemented validation for report submissions, including required fields and email format.
- Added tests for content reporting functionality, covering various scenarios and edge cases.
- Updated frontend API utility to include new content report methods.
- Bumped app version to 0.8.87 and updated relevant page versions.
2026-05-11 17:54:53 +02:00

328 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"
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"}[k]
existing_row.keys = lambda: ["id", "status"]
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"