From 2b1a7cccfbc6f25cb919fe6b8bae8bcb37a68585 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 7 Mar 2026 14:21:29 +0100 Subject: [PATCH] fase 4 afgerond --- app/api/session.py | 10 ++ app/services/session_service.py | 31 ++++++ feature_tests_mapping_preview.sh | 165 +++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100755 feature_tests_mapping_preview.sh diff --git a/app/api/session.py b/app/api/session.py index 938a353..604bc5c 100644 --- a/app/api/session.py +++ b/app/api/session.py @@ -137,3 +137,13 @@ def reorder_selected_files( raise HTTPException(status_code=400, detail=str(exc)) return {"session_id": normalized_session_id, "items": items} + + +@router.get("/mapping-preview") +def get_mapping_preview(session_id: str = Query("default", min_length=1)): + service = SessionService() + normalized_session_id = _normalize_session_id(session_id) + try: + return service.build_mapping_preview(normalized_session_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) diff --git a/app/services/session_service.py b/app/services/session_service.py index f9942bf..5acbb90 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -316,3 +316,34 @@ class SessionService: (position, session_id, item["selection_id"]), ) return self.list_selected_files(session_id) + + def build_mapping_preview(self, session_id: str) -> dict: + episodes = self.list_selected_episodes(session_id) + files = self.list_selected_files(session_id) + + if len(episodes) != len(files): + raise ValueError( + "Selected episodes and selected files count mismatch: " + f"{len(episodes)} episodes vs {len(files)} files" + ) + + mappings = [] + for index, (episode_item, file_item) in enumerate(zip(episodes, files)): + mappings.append( + { + "index": index, + "episode_selection_id": episode_item["selection_id"], + "file_selection_id": file_item["selection_id"], + "episode": episode_item["episode"], + "file": file_item["file"], + } + ) + + return { + "session_id": session_id, + "counts": { + "episodes": len(episodes), + "files": len(files), + }, + "mappings": mappings, + } diff --git a/feature_tests_mapping_preview.sh b/feature_tests_mapping_preview.sh new file mode 100755 index 0000000..79a5781 --- /dev/null +++ b/feature_tests_mapping_preview.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -z "${BASE_URL:-}" ]; then + if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://127.0.0.1:8085" + elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then + BASE_URL="http://host.containers.internal:8085" + else + echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2 + exit 1 + fi +fi + +SESSION_ID="mapping-preview-test-$(date +%s)-$$" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +curl --fail --silent --show-error -X DELETE \ + "${BASE_URL}/api/session/selected-episodes?session_id=${SESSION_ID}" \ + >/dev/null +curl --fail --silent --show-error -X DELETE \ + "${BASE_URL}/api/session/selected-files?session_id=${SESSION_ID}" \ + >/dev/null + +echo "== Feature test 1: 1-op-1 mapping preview returns ordered mappings ==" +cat > "${TMP_DIR}/episodes_payload.json" <<'JSON' +{ + "items": [ + { + "id": 9784113, + "season_number": 1, + "episode_number": 1, + "title": "Pilot", + "label": "S01E01 - Pilot - 2024-02-29" + }, + { + "id": 10347197, + "season_number": 1, + "episode_number": 2, + "title": "A Classic New York Character", + "label": "S01E02 - A Classic New York Character - 2024-04-04" + } + ] +} +JSON + +cat > "${TMP_DIR}/files_payload.json" <<'JSON' +{ + "items": [ + { + "path": "/Volumes/8TB/Shared_Folders/TV_Shows/Elsbeth/ep1.mkv", + "name": "ep1.mkv" + }, + { + "path": "/Volumes/8TB/Shared_Folders/TV_Shows/Elsbeth/ep2.mkv", + "name": "ep2.mkv" + } + ] +} +JSON + +curl --fail --silent --show-error \ + -X POST "${BASE_URL}/api/session/selected-episodes?session_id=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + --data @"${TMP_DIR}/episodes_payload.json" \ + >/dev/null + +curl --fail --silent --show-error \ + -X POST "${BASE_URL}/api/session/selected-files?session_id=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + --data @"${TMP_DIR}/files_payload.json" \ + >/dev/null + +curl --fail --silent --show-error \ + "${BASE_URL}/api/session/mapping-preview?session_id=${SESSION_ID}" \ + -o "${TMP_DIR}/preview.json" + +cat "${TMP_DIR}/preview.json" + +python3 - "${TMP_DIR}/preview.json" <<'PY' +import json +import sys +from pathlib import Path + +data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +assert isinstance(data, dict), "preview response must be an object" +assert data.get("counts", {}).get("episodes") == 2, "expected 2 episodes in counts" +assert data.get("counts", {}).get("files") == 2, "expected 2 files in counts" +assert isinstance(data.get("mappings"), list), "mappings must be a list" +assert len(data["mappings"]) == 2, "expected 2 mappings" +assert data["mappings"][0]["episode"]["title"] == "Pilot", "first mapped episode mismatch" +assert data["mappings"][0]["file"]["name"] == "ep1.mkv", "first mapped file mismatch" +assert data["mappings"][1]["episode"]["title"] == "A Classic New York Character", "second mapped episode mismatch" +assert data["mappings"][1]["file"]["name"] == "ep2.mkv", "second mapped file mismatch" +print("mapping preview validation passed") +PY + +echo +echo "== Feature test 2: count mismatch returns clear HTTP 400 ==" +curl --fail --silent --show-error -X DELETE \ + "${BASE_URL}/api/session/selected-files?session_id=${SESSION_ID}" \ + >/dev/null + +cat > "${TMP_DIR}/one_file_payload.json" <<'JSON' +{ + "items": [ + { + "path": "/Volumes/8TB/Shared_Folders/TV_Shows/Elsbeth/ep1.mkv", + "name": "ep1.mkv" + } + ] +} +JSON + +curl --fail --silent --show-error \ + -X POST "${BASE_URL}/api/session/selected-files?session_id=${SESSION_ID}" \ + -H "Content-Type: application/json" \ + --data @"${TMP_DIR}/one_file_payload.json" \ + >/dev/null + +curl --silent --show-error \ + -o "${TMP_DIR}/preview_mismatch.json" \ + -w "%{http_code}" \ + "${BASE_URL}/api/session/mapping-preview?session_id=${SESSION_ID}" \ + > "${TMP_DIR}/preview_mismatch.status" + +cat "${TMP_DIR}/preview_mismatch.json" + +python3 - "${TMP_DIR}/preview_mismatch.status" "${TMP_DIR}/preview_mismatch.json" <<'PY' +import json +import sys +from pathlib import Path + +status = Path(sys.argv[1]).read_text(encoding="utf-8").strip() +data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) +assert status == "400", f"expected HTTP 400, got {status}" +assert "detail" in data, "error response missing detail" +assert "count mismatch" in data["detail"], "detail should mention count mismatch" +print("mismatch validation passed") +PY + +echo +echo "== Feature test 3: preview endpoint is read-only (no mutation) ==" +curl --fail --silent --show-error \ + "${BASE_URL}/api/session/selected-episodes?session_id=${SESSION_ID}" \ + -o "${TMP_DIR}/episodes_after.json" +curl --fail --silent --show-error \ + "${BASE_URL}/api/session/selected-files?session_id=${SESSION_ID}" \ + -o "${TMP_DIR}/files_after.json" + +python3 - "${TMP_DIR}/episodes_after.json" "${TMP_DIR}/files_after.json" <<'PY' +import json +import sys +from pathlib import Path + +episodes = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +files = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) +assert len(episodes.get("items", [])) == 2, "episodes were unexpectedly mutated" +assert len(files.get("items", [])) == 1, "files were unexpectedly mutated" +print("read-only validation passed") +PY + +echo +echo "All mapping preview feature tests passed."