fix: resolve relative folder paths against anime dir in delete_series
The folder column in anime_series stores the relative folder name
(e.g. 'Beyblade Burst (2016)'), not an absolute path. The old
delete_series code called os.path.abspath(folder_path) directly,
which joins a relative path against the process current working
directory. Inside the container the FastAPI app runs with CWD=/app
while the anime directory is /data, so 'Beyblade Burst (2016)'
resolved to '/app/Beyblade Burst (2016)' and the is_safe_path check
correctly (but unhelpfully) flagged it as outside the /data base,
skipping the folder delete while still removing the row.
Fix: resolve relative folder paths against the configured anime
directory before validating against the base. Absolute paths still
work unchanged.
Also harden is_safe_path() the same way so a relative target is
treated as relative to base_path, not to the process CWD. Path
traversal ('../etc/passwd') is still rejected.
Adds two regression tests:
- is_safe_path with chdir to '/' and relative target resolves
against the base
- delete_series with chdir to '/', relative folder in DB,
succeeds and removes the folder
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Unit tests for AnimeService.delete_series()."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -328,6 +329,69 @@ class TestDeleteSeriesService:
|
||||
assert result.folder_error is not None
|
||||
assert "outside" in result.folder_error.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_series_relative_folder_with_different_cwd(
|
||||
self, anime_service, tmp_path
|
||||
):
|
||||
"""Regression: delete_series must work when the stored folder is relative
|
||||
and the process CWD differs from directory_to_search.
|
||||
|
||||
In the container the FastAPI app runs with CWD=/app while the anime
|
||||
directory is /data. The DB stores the relative folder name (e.g.
|
||||
"Beyblade Burst (2016)"). The old code called
|
||||
``os.path.abspath(folder)`` which joined against CWD=/app and
|
||||
produced "/app/Beyblade Burst (2016)", which was then rejected as
|
||||
outside the /data base. The fix resolves relative paths against
|
||||
the configured anime directory instead.
|
||||
"""
|
||||
safe_base = tmp_path / "data"
|
||||
safe_base.mkdir()
|
||||
series_folder = safe_base / "Beyblade Burst (2016)"
|
||||
series_folder.mkdir()
|
||||
|
||||
anime_service._directory = str(safe_base)
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_ctx = _make_db_ctx(mock_session)
|
||||
|
||||
# Stored folder is RELATIVE (matches what's actually in the DB)
|
||||
mock_series = MagicMock()
|
||||
mock_series.key = "beyblade-burst"
|
||||
mock_series.name = "Beyblade Burst"
|
||||
mock_series.folder = "Beyblade Burst (2016)"
|
||||
mock_series.id = 336
|
||||
|
||||
# Simulate process CWD differing from anime dir (container case:
|
||||
# CWD=/app while anime dir is /data). Use "/" as a stable, always-
|
||||
# existing CWD distinct from tmp_path.
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir("/")
|
||||
with patch(
|
||||
"src.server.database.connection.get_db_session",
|
||||
return_value=mock_ctx,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.get_by_key",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_series,
|
||||
):
|
||||
result = await anime_service.delete_series(
|
||||
key="beyblade-burst",
|
||||
delete_database=False,
|
||||
delete_folder=True,
|
||||
)
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
# Folder MUST be deleted successfully
|
||||
assert result.deleted_folder is True, (
|
||||
f"folder delete failed: success={result.success} "
|
||||
f"folder_error={result.folder_error!r}"
|
||||
)
|
||||
assert result.folder_error is None
|
||||
assert result.success is True
|
||||
assert not series_folder.exists()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Error handling
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user