147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from mjr_am_backend.features.assets.delete_service import delete_asset_and_cleanup
|
|
from mjr_am_backend.features.assets.models import AssetDeleteTarget, AssetRenameTarget
|
|
from mjr_am_backend.features.assets.rename_service import rename_asset_and_sync
|
|
from mjr_am_backend.shared import Result
|
|
|
|
|
|
class _DB:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple]] = []
|
|
self.query_result: Result[Any] = Result.Ok([])
|
|
self.execute_result: Result[Any] = Result.Ok(1)
|
|
|
|
@asynccontextmanager
|
|
async def atransaction(self, mode="immediate"):
|
|
self.executed.append(("TRANSACTION", (mode,)))
|
|
yield
|
|
|
|
async def aexecute(self, sql, params=()):
|
|
self.executed.append((sql, tuple(params)))
|
|
return self.execute_result
|
|
|
|
async def aquery(self, sql, params=()):
|
|
self.executed.append((sql, tuple(params)))
|
|
return self.query_result
|
|
|
|
|
|
class _Index:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple] = []
|
|
|
|
async def index_paths(self, *args):
|
|
self.calls.append(args)
|
|
return Result.Ok({})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_asset_and_cleanup_removes_file_and_db_rows(tmp_path: Path):
|
|
file_path = tmp_path / "asset.png"
|
|
file_path.write_bytes(b"x")
|
|
db = _DB()
|
|
target = AssetDeleteTarget(
|
|
services={"db": db},
|
|
matched_asset_id=7,
|
|
resolved_path=file_path,
|
|
filepath_where="filepath IN (?) OR filepath = ? COLLATE NOCASE",
|
|
filepath_params=(str(file_path), str(file_path)),
|
|
)
|
|
|
|
def _delete_file(path: Path) -> Result[bool]:
|
|
path.unlink()
|
|
return Result.Ok(True)
|
|
|
|
result = await delete_asset_and_cleanup(
|
|
services={"db": db},
|
|
target=target,
|
|
delete_file_safe=_delete_file,
|
|
safe_error_message=lambda exc, default: f"{default}: {exc}",
|
|
)
|
|
|
|
assert result.ok is True
|
|
assert file_path.exists() is False
|
|
assert any("DELETE FROM assets WHERE id = ?" in sql for sql, _ in db.executed)
|
|
payload = result.unwrap()
|
|
assert payload["db_cleanup_ok"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_asset_and_cleanup_reports_db_cleanup_failure(tmp_path: Path):
|
|
file_path = tmp_path / "asset.png"
|
|
file_path.write_bytes(b"x")
|
|
db = _DB()
|
|
db.execute_result = Result.Err("DB_ERROR", "locked")
|
|
target = AssetDeleteTarget(
|
|
services={"db": db},
|
|
matched_asset_id=7,
|
|
resolved_path=file_path,
|
|
filepath_where="filepath IN (?) OR filepath = ? COLLATE NOCASE",
|
|
filepath_params=(str(file_path), str(file_path)),
|
|
)
|
|
|
|
def _delete_file(path: Path) -> Result[bool]:
|
|
path.unlink()
|
|
return Result.Ok(True)
|
|
|
|
result = await delete_asset_and_cleanup(
|
|
services={"db": db},
|
|
target=target,
|
|
delete_file_safe=_delete_file,
|
|
safe_error_message=lambda exc, default: f"{default}: {exc}",
|
|
)
|
|
|
|
assert result.ok is True
|
|
assert file_path.exists() is False
|
|
payload = result.unwrap()
|
|
assert payload["deleted"] == 1
|
|
assert payload["db_cleanup_ok"] is False
|
|
assert payload["db_errors"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rename_asset_and_sync_updates_file_db_and_reindex(tmp_path: Path):
|
|
old_path = tmp_path / "old.png"
|
|
old_path.write_bytes(b"x")
|
|
db = _DB()
|
|
db.query_result = Result.Ok([{"id": 1, "filename": "new.png"}])
|
|
index = _Index()
|
|
target = AssetRenameTarget(
|
|
services={"db": db, "index": index},
|
|
matched_asset_id=1,
|
|
current_resolved=old_path,
|
|
current_filename="old.png",
|
|
current_source="output",
|
|
current_root_id="",
|
|
filepath_where="filepath IN (?) OR filepath = ? COLLATE NOCASE",
|
|
filepath_params=(str(old_path), str(old_path)),
|
|
new_name="new.png",
|
|
)
|
|
|
|
result = await rename_asset_and_sync(
|
|
services={"db": db, "index": index},
|
|
target=target,
|
|
infer_source_and_root_id_from_path=lambda *args, **kwargs: _raise_not_used(),
|
|
is_within_root=lambda _p, _r: True,
|
|
get_runtime_output_root=lambda: str(tmp_path),
|
|
get_input_directory=lambda: str(tmp_path / "in"),
|
|
list_custom_roots=lambda: [],
|
|
to_thread_timeout_s=5,
|
|
safe_error_message=lambda exc, default: f"{default}: {exc}",
|
|
)
|
|
|
|
assert result.ok is True
|
|
assert (tmp_path / "new.png").exists() is True
|
|
assert old_path.exists() is False
|
|
assert any("UPDATE assets SET filename = ?, filepath = ?, mtime = ? WHERE id = ?" in sql for sql, _ in db.executed)
|
|
assert len(index.calls) == 1
|
|
|
|
|
|
def _raise_not_used():
|
|
raise AssertionError("should not be called")
|