feat: Implement hash-first identification strategy in audio file syncing and enhance tests for renamed files
Some checks failed
Backend CI / lint (push) Failing after 4m55s
Backend CI / test (push) Failing after 4m32s

This commit is contained in:
JSC
2025-08-25 11:56:07 +02:00
parent d81a54207c
commit da66516bb3
3 changed files with 316 additions and 68 deletions

View File

@@ -152,10 +152,15 @@ class TestSoundScannerService:
"errors": 0,
"files": [],
}
# Set the existing sound filename to match temp file for "unchanged" test
existing_sound.filename = temp_path.name
await scanner_service._sync_audio_file(
temp_path,
"SDB",
existing_sound,
existing_sound, # existing_sound_by_hash (same hash)
None, # existing_sound_by_filename (no conflict)
"same_hash",
results,
)
@@ -168,6 +173,121 @@ class TestSoundScannerService:
finally:
temp_path.unlink()
@pytest.mark.asyncio
async def test_sync_audio_file_renamed(self, scanner_service) -> None:
"""Test syncing file that was renamed (same hash, different filename)."""
# Existing sound with same hash but different filename
existing_sound = Sound(
id=1,
type="SDB",
name="Old Name",
filename="old_name.mp3",
duration=120000,
size=1024,
hash="same_hash",
)
scanner_service.sound_repo.update = AsyncMock(return_value=existing_sound)
# Mock file operations to return same hash
with (
patch("app.services.sound_scanner.get_file_hash", return_value="same_hash"),
patch("app.services.sound_scanner.get_audio_duration", return_value=120000),
patch("app.services.sound_scanner.get_file_size", return_value=1024),
):
# Create a temporary file with different name
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
temp_path = Path(f.name)
try:
results = {
"scanned": 0,
"added": 0,
"updated": 0,
"deleted": 0,
"skipped": 0,
"errors": 0,
"files": [],
}
await scanner_service._sync_audio_file(
temp_path,
"SDB",
existing_sound, # existing_sound_by_hash (same hash)
None, # existing_sound_by_filename (different filename)
"same_hash",
results,
)
# Should be marked as updated (renamed)
assert results["updated"] == 1
assert results["added"] == 0
assert results["skipped"] == 0
assert len(results["files"]) == 1
assert results["files"][0]["status"] == "updated"
assert results["files"][0]["reason"] == "file was renamed"
assert results["files"][0]["changes"] == ["filename", "name"]
# Verify update was called with new filename
scanner_service.sound_repo.update.assert_called_once()
call_args = scanner_service.sound_repo.update.call_args[0][1] # update_data
assert call_args["filename"] == temp_path.name
finally:
temp_path.unlink()
@pytest.mark.asyncio
async def test_scan_directory_rename_no_delete(self, scanner_service, mock_session) -> None:
"""Test that renamed files are not deleted (regression test)."""
# Create a mock existing sound that will be "renamed"
existing_sound = Sound(
id=1,
type="SDB",
name="Old Name",
filename="old_name.mp3",
duration=120000,
size=1024,
hash="same_hash",
)
# Mock the repository to return the existing sound
scanner_service.sound_repo.get_by_type = AsyncMock(return_value=[existing_sound])
scanner_service.sound_repo.update = AsyncMock()
scanner_service.sound_repo.delete = AsyncMock()
# Create temporary directory with renamed file
import tempfile
import os
with tempfile.TemporaryDirectory() as temp_dir:
# Create the "renamed" file (same hash, different name)
new_file_path = os.path.join(temp_dir, "new_name.mp3")
with open(new_file_path, "wb") as f:
f.write(b"test audio content") # This will produce consistent hash
# Mock file operations to return same hash
with (
patch("app.services.sound_scanner.get_file_hash", return_value="same_hash"),
patch("app.services.sound_scanner.get_audio_duration", return_value=120000),
patch("app.services.sound_scanner.get_file_size", return_value=1024),
):
results = await scanner_service.scan_directory(temp_dir, "SDB")
# Should have detected one renamed file
assert results["updated"] == 1
assert results["deleted"] == 0 # This is the key assertion - no deletion!
assert results["added"] == 0
assert len(results["files"]) == 1
# Verify it was marked as renamed
file_result = results["files"][0]
assert file_result["status"] == "updated"
assert file_result["reason"] == "file was renamed"
# Verify update was called but delete was NOT called
scanner_service.sound_repo.update.assert_called_once()
scanner_service.sound_repo.delete.assert_not_called()
@pytest.mark.asyncio
async def test_sync_audio_file_new(self, scanner_service) -> None:
"""Test syncing a new audio file."""
@@ -202,7 +322,14 @@ class TestSoundScannerService:
"errors": 0,
"files": [],
}
await scanner_service._sync_audio_file(temp_path, "SDB", None, results)
await scanner_service._sync_audio_file(
temp_path,
"SDB",
None, # existing_sound_by_hash
None, # existing_sound_by_filename
"test_hash",
results,
)
assert results["added"] == 1
assert results["skipped"] == 0
@@ -262,7 +389,9 @@ class TestSoundScannerService:
await scanner_service._sync_audio_file(
temp_path,
"SDB",
existing_sound,
None, # existing_sound_by_hash (different hash)
existing_sound, # existing_sound_by_filename
"new_hash",
results,
)
@@ -325,7 +454,9 @@ class TestSoundScannerService:
await scanner_service._sync_audio_file(
temp_path,
"CUSTOM",
None,
None, # existing_sound_by_hash
None, # existing_sound_by_filename
"custom_hash",
results,
)