All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 31s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 27s
- Added compliance implementation report detailing the status of various packages (P-03, P-04, P-05, P-07, P-23, P-24) and their technical changes, tests, and notes. - Introduced a new workspace configuration file for the project to streamline development setup.
253 lines
9.4 KiB
Python
253 lines
9.4 KiB
Python
"""
|
|
P-04: Copyright-Pflicht bei Promotion auf club/official.
|
|
PATCH /api/media-assets/{id} und POST /api/media-assets/bulk-patch
|
|
lehnen eine Sichtbarkeits-Promotion auf club oder official ab,
|
|
wenn keine copyright_notice vorhanden ist.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from contextlib import ExitStack
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
os.environ.setdefault("SKIP_DB_MIGRATE", "1")
|
|
|
|
from main import app
|
|
from tenant_context import TenantContext, get_tenant_context
|
|
|
|
_SUPERADMIN_TENANT = TenantContext(
|
|
profile_id=1,
|
|
global_role="superadmin",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
_PRIVATE_ASSET: dict = {
|
|
"id": 42,
|
|
"visibility": "private",
|
|
"club_id": 7,
|
|
"uploaded_by_profile_id": 1,
|
|
"lifecycle_state": "active",
|
|
"copyright_notice": None,
|
|
"original_filename": "foto.jpg",
|
|
"sha256": "a" * 64,
|
|
"storage_key": f"library/verein-7/image/{'a' * 64}.jpg",
|
|
"storage_backend": "local",
|
|
"mime_type": "image/jpeg",
|
|
"byte_size": 1024,
|
|
"created_at": None,
|
|
"tags": [],
|
|
}
|
|
|
|
_ASSET_WITH_COPYRIGHT: dict = {**_PRIVATE_ASSET, "copyright_notice": "Verein 2026"}
|
|
|
|
|
|
@pytest.fixture
|
|
def client() -> TestClient:
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_overrides():
|
|
yield
|
|
app.dependency_overrides.pop(get_tenant_context, None)
|
|
|
|
|
|
def _make_db_mocks(asset: dict) -> tuple[MagicMock, MagicMock]:
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.return_value = asset
|
|
mock_conn = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__.return_value = mock_conn
|
|
mock_cm.__exit__.return_value = False
|
|
return mock_cm, mock_cur
|
|
|
|
|
|
_PERMISSION_PATCHES = [
|
|
("routers.media_assets.assert_can_edit_media_asset_metadata", {}),
|
|
("routers.media_assets.assert_valid_governance_visibility", {}),
|
|
("routers.media_assets._media_assets_tags_column_present", {"return_value": False}),
|
|
("routers.media_assets.get_effective_media_root", {"return_value": "/tmp/media"}),
|
|
("routers.media_assets._relocate_asset_file_if_governance_changed", {"return_value": None}),
|
|
]
|
|
|
|
|
|
def _enter_permission_patches(stack: ExitStack) -> None:
|
|
for target, kwargs in _PERMISSION_PATCHES:
|
|
stack.enter_context(patch(target, **kwargs))
|
|
|
|
|
|
# ── Single PATCH ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_patch_promote_to_club_without_copyright_returns_400(client: TestClient) -> None:
|
|
"""Promotion private -> club ohne copyright_notice muss 400 liefern."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cm, mock_cur = _make_db_mocks(_PRIVATE_ASSET)
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.patch(
|
|
"/api/media-assets/42",
|
|
json={"visibility": "club", "club_id": 7},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code == 400
|
|
detail = r.json()["detail"].lower()
|
|
assert "copyright" in detail or "urheberrecht" in detail
|
|
|
|
|
|
def test_patch_promote_to_official_without_copyright_returns_400(client: TestClient) -> None:
|
|
"""Promotion private -> official ohne copyright_notice muss 400 liefern."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cm, mock_cur = _make_db_mocks(_PRIVATE_ASSET)
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.patch(
|
|
"/api/media-assets/42",
|
|
json={"visibility": "official"},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code == 400
|
|
detail = r.json()["detail"].lower()
|
|
assert "copyright" in detail or "urheberrecht" in detail
|
|
|
|
|
|
def test_patch_promote_to_club_with_copyright_in_body_allowed(client: TestClient) -> None:
|
|
"""Promotion private -> club MIT copyright_notice im Body darf nicht 400 liefern."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cm, mock_cur = _make_db_mocks(_PRIVATE_ASSET)
|
|
mock_cur.fetchone.side_effect = [
|
|
_PRIVATE_ASSET,
|
|
{**_PRIVATE_ASSET, "visibility": "club", "copyright_notice": "Verein 2026"},
|
|
]
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.patch(
|
|
"/api/media-assets/42",
|
|
json={"visibility": "club", "club_id": 7, "copyright_notice": "Verein 2026"},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code != 400 or "copyright" not in r.json().get("detail", "").lower()
|
|
|
|
|
|
def test_patch_promote_to_club_existing_copyright_allowed(client: TestClient) -> None:
|
|
"""Asset hat bereits copyright_notice -> Promotion ohne Body-Copyright erlaubt."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cm, mock_cur = _make_db_mocks(_ASSET_WITH_COPYRIGHT)
|
|
mock_cur.fetchone.side_effect = [
|
|
_ASSET_WITH_COPYRIGHT,
|
|
{**_ASSET_WITH_COPYRIGHT, "visibility": "club"},
|
|
]
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.patch(
|
|
"/api/media-assets/42",
|
|
json={"visibility": "club", "club_id": 7},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code != 400 or "copyright" not in r.json().get("detail", "").lower()
|
|
|
|
|
|
def test_patch_filename_only_no_copyright_check(client: TestClient) -> None:
|
|
"""Kein Visibility-Wechsel -> keine Copyright-Prufung."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cm, mock_cur = _make_db_mocks(_PRIVATE_ASSET)
|
|
mock_cur.fetchone.side_effect = [
|
|
_PRIVATE_ASSET,
|
|
{**_PRIVATE_ASSET, "original_filename": "neu.jpg"},
|
|
]
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.patch(
|
|
"/api/media-assets/42",
|
|
json={"original_filename": "neu.jpg"},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
|
# ── Bulk PATCH ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_bulk_patch_promote_to_club_without_copyright_in_failed(client: TestClient) -> None:
|
|
"""Bulk-Promotion ohne copyright_notice -> Asset in failed-Liste."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.return_value = _PRIVATE_ASSET
|
|
mock_conn = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__.return_value = mock_conn
|
|
mock_cm.__exit__.return_value = False
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.post(
|
|
"/api/media-assets/bulk-patch",
|
|
json={"media_asset_ids": [42], "visibility": "club", "club_id": 7},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["updated_count"] == 0
|
|
assert body["failed_count"] == 1
|
|
detail = body["failed"][0]["detail"].lower()
|
|
assert "copyright" in detail or "urheberrecht" in detail
|
|
|
|
|
|
def test_bulk_patch_promote_to_club_with_copyright_in_updated(client: TestClient) -> None:
|
|
"""Bulk-Promotion MIT copyright_notice -> Asset in updated-Liste."""
|
|
app.dependency_overrides[get_tenant_context] = lambda: _SUPERADMIN_TENANT
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.return_value = _PRIVATE_ASSET
|
|
mock_conn = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__.return_value = mock_conn
|
|
mock_cm.__exit__.return_value = False
|
|
|
|
with ExitStack() as stack:
|
|
stack.enter_context(patch("routers.media_assets.get_db", return_value=mock_cm))
|
|
stack.enter_context(patch("routers.media_assets.get_cursor", return_value=mock_cur))
|
|
_enter_permission_patches(stack)
|
|
r = client.post(
|
|
"/api/media-assets/bulk-patch",
|
|
json={
|
|
"media_asset_ids": [42],
|
|
"visibility": "club",
|
|
"club_id": 7,
|
|
"copyright_notice": "Verein 2026",
|
|
},
|
|
headers={"X-Auth-Token": "t"},
|
|
)
|
|
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert 42 in body["updated"]
|
|
assert body["updated_count"] == 1
|