All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 24s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 27s
- Updated project status to reflect the latest media management milestones and version increment to 0.8.44. - Enhanced MEDIA_ASSETS_AND_ARCHIVE_SPEC.md with new API details for media asset lifecycle and inline media integration. - Improved exercise media handling in the frontend, including new preview features and user prompts for media deletion. - Adjusted backend API to ensure proper handling of media asset deletions without removing files, maintaining governance and user experience.
274 lines
8.4 KiB
Python
274 lines
8.4 KiB
Python
"""
|
|
Medienarchiv: GET /api/media-assets und POST /api/exercises/{id}/media/from-asset (gemockte DB).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
os.environ.setdefault("SKIP_DB_MIGRATE", "1")
|
|
|
|
from auth import require_auth
|
|
from main import app
|
|
from tenant_context import TenantContext, get_tenant_context
|
|
|
|
|
|
@pytest.fixture
|
|
def client() -> TestClient:
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_overrides() -> None:
|
|
yield
|
|
app.dependency_overrides.pop(require_auth, None)
|
|
app.dependency_overrides.pop(get_tenant_context, None)
|
|
|
|
|
|
def _mock_db(mock_cur: MagicMock) -> MagicMock:
|
|
mock_conn = MagicMock()
|
|
mock_cm = MagicMock()
|
|
mock_cm.__enter__.return_value = mock_conn
|
|
mock_cm.__exit__.return_value = False
|
|
return mock_cm
|
|
|
|
|
|
def test_list_media_assets_ok_mocked(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 10, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=10,
|
|
global_role="trainer",
|
|
effective_club_id=5,
|
|
club_ids=frozenset({5}),
|
|
memberships=[],
|
|
)
|
|
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchall.return_value = [
|
|
{
|
|
"id": 1,
|
|
"mime_type": "image/png",
|
|
"byte_size": 100,
|
|
"original_filename": "a.png",
|
|
"visibility": "official",
|
|
"club_id": None,
|
|
"uploaded_by_profile_id": 2,
|
|
"lifecycle_state": "active",
|
|
"created_at": None,
|
|
"sha256": "a" * 64,
|
|
}
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.media_assets.get_db", return_value=mock_cm), patch(
|
|
"routers.media_assets.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.get("/api/media-assets?q=test", headers={"X-Auth-Token": "t"})
|
|
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["limit"] == 30
|
|
assert len(body["items"]) == 1
|
|
assert body["items"][0]["original_filename"] == "a.png"
|
|
|
|
|
|
def test_attach_from_asset_duplicate_returns_400(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=1,
|
|
global_role="trainer",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.side_effect = [
|
|
{"created_by": 1, "visibility": "private", "club_id": None},
|
|
{"c": 0},
|
|
{
|
|
"id": 5,
|
|
"mime_type": "image/jpeg",
|
|
"byte_size": 10,
|
|
"original_filename": "x.jpg",
|
|
"visibility": "official",
|
|
"club_id": None,
|
|
"uploaded_by_profile_id": 1,
|
|
"lifecycle_state": "active",
|
|
"storage_key": "exercises/x.jpg",
|
|
},
|
|
{"id": 1},
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.exercises.get_db", return_value=mock_cm), patch(
|
|
"routers.exercises.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.post(
|
|
"/api/exercises/3/media/from-asset",
|
|
headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
|
|
json={
|
|
"media_asset_id": 5,
|
|
"title": "",
|
|
"description": "",
|
|
"context": "ablauf",
|
|
"is_primary": False,
|
|
},
|
|
)
|
|
|
|
assert r.status_code == 400
|
|
assert "bereits" in (r.json().get("detail") or "").lower()
|
|
|
|
|
|
def test_attach_from_asset_ok_mocked(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=1,
|
|
global_role="trainer",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
inserted = {
|
|
"id": 99,
|
|
"exercise_id": 3,
|
|
"media_type": "image",
|
|
"file_path": "/media/exercises/h.jpg",
|
|
"file_size": 10,
|
|
"mime_type": "image/jpeg",
|
|
"original_filename": "h.jpg",
|
|
"embed_url": None,
|
|
"embed_platform": None,
|
|
"title": "h.jpg",
|
|
"description": None,
|
|
"sort_order": 1,
|
|
"is_primary": False,
|
|
"context": "ablauf",
|
|
"created_at": None,
|
|
"media_asset_id": 5,
|
|
}
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.side_effect = [
|
|
{"created_by": 1, "visibility": "private", "club_id": None},
|
|
{"c": 0},
|
|
{
|
|
"id": 5,
|
|
"mime_type": "image/jpeg",
|
|
"byte_size": 10,
|
|
"original_filename": "h.jpg",
|
|
"visibility": "official",
|
|
"club_id": None,
|
|
"uploaded_by_profile_id": 1,
|
|
"lifecycle_state": "active",
|
|
"storage_key": "exercises/h.jpg",
|
|
},
|
|
None,
|
|
inserted,
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.exercises.get_db", return_value=mock_cm), patch(
|
|
"routers.exercises.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.post(
|
|
"/api/exercises/3/media/from-asset",
|
|
headers={"X-Auth-Token": "t", "Content-Type": "application/json"},
|
|
json={
|
|
"media_asset_id": 5,
|
|
"context": "detail",
|
|
"is_primary": False,
|
|
},
|
|
)
|
|
|
|
assert r.status_code == 201
|
|
body = r.json()
|
|
assert body["id"] == 99
|
|
assert body["media_asset_id"] == 5
|
|
assert body["asset_lifecycle_state"] == "active"
|
|
|
|
|
|
def test_delete_exercise_media_returns_orphan_when_last_ref(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=1,
|
|
global_role="trainer",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.side_effect = [
|
|
{"created_by": 1, "visibility": "private", "club_id": None},
|
|
{"media_asset_id": 88},
|
|
{"c": 1},
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.exercises.get_db", return_value=mock_cm), patch(
|
|
"routers.exercises.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.delete("/api/exercises/10/media/20", headers={"X-Auth-Token": "t"})
|
|
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["ok"] is True
|
|
assert body["orphan_media_asset_id"] == 88
|
|
|
|
|
|
def test_delete_exercise_media_no_orphan_when_shared(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=1,
|
|
global_role="trainer",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.side_effect = [
|
|
{"created_by": 1, "visibility": "private", "club_id": None},
|
|
{"media_asset_id": 88},
|
|
{"c": 2},
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.exercises.get_db", return_value=mock_cm), patch(
|
|
"routers.exercises.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.delete("/api/exercises/10/media/20", headers={"X-Auth-Token": "t"})
|
|
|
|
assert r.status_code == 200
|
|
assert r.json()["orphan_media_asset_id"] is None
|
|
|
|
|
|
def test_delete_exercise_media_embed_row_no_orphan(client: TestClient) -> None:
|
|
app.dependency_overrides[require_auth] = lambda: {"profile_id": 1, "role": "trainer"}
|
|
app.dependency_overrides[get_tenant_context] = lambda: TenantContext(
|
|
profile_id=1,
|
|
global_role="trainer",
|
|
effective_club_id=None,
|
|
club_ids=frozenset(),
|
|
memberships=[],
|
|
)
|
|
|
|
mock_cur = MagicMock()
|
|
mock_cur.fetchone.side_effect = [
|
|
{"created_by": 1, "visibility": "private", "club_id": None},
|
|
{"media_asset_id": None},
|
|
]
|
|
mock_cm = _mock_db(mock_cur)
|
|
|
|
with patch("routers.exercises.get_db", return_value=mock_cm), patch(
|
|
"routers.exercises.get_cursor", return_value=mock_cur
|
|
):
|
|
r = client.delete("/api/exercises/10/media/21", headers={"X-Auth-Token": "t"})
|
|
|
|
assert r.status_code == 200
|
|
assert r.json() == {"ok": True, "orphan_media_asset_id": None}
|