feat: Implement hash-first identification strategy in audio file syncing and enhance tests for renamed files
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user