rommapp_romm/backend/tests/handler/filesystem/test_resources_handler.py
Michael Manganiello ba21cbc1e1
misc: Separate tests folder from backend code
Create separate `tests/` folder for all tests. This will also simplify
not copying tests code into the Docker image.
2025-08-08 12:49:13 -03:00

487 lines
20 KiB
Python

import os
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from config import RESOURCES_BASE_PATH
from handler.filesystem.base_handler import CoverSize
from handler.filesystem.resources_handler import FSResourcesHandler
from models.collection import Collection
from models.rom import Rom
class TestFSResourcesHandler:
"""Test suite for FSResourcesHandler class"""
@pytest.fixture
def handler(self):
return FSResourcesHandler()
@pytest.fixture
def rom(self):
"""Create a mock ROM object for testing"""
rom = Mock(spec=Rom)
rom.id = 1
rom.platform_id = 1
rom.fs_resources_path = "roms/1/1"
return rom
def test_init_uses_resources_base_path(self, handler: FSResourcesHandler):
"""Test that FSResourcesHandler initializes with RESOURCES_BASE_PATH"""
assert handler.base_path == Path(RESOURCES_BASE_PATH).resolve()
def test_get_platform_resources_path(self, handler: FSResourcesHandler):
"""Test get_platform_resources_path method"""
platform_id = 1
result = handler.get_platform_resources_path(platform_id)
expected = os.path.join("roms", "1")
assert result == expected
def test_get_platform_resources_path_different_ids(
self, handler: FSResourcesHandler
):
"""Test get_platform_resources_path with different platform IDs"""
test_ids = [1, 42, 123, 999]
for platform_id in test_ids:
result = handler.get_platform_resources_path(platform_id)
expected = os.path.join("roms", str(platform_id))
assert result == expected
def test_cover_exists_no_cover(self, handler: FSResourcesHandler, rom: Rom):
"""Test cover_exists when no cover exists"""
# Test with non-existent covers
assert not handler.cover_exists(rom, CoverSize.SMALL)
assert not handler.cover_exists(rom, CoverSize.BIG)
def test_cover_exists_with_existing_covers(self, handler: FSResourcesHandler):
"""Test cover_exists with existing covers from the test data"""
# Use existing test data structure - check directories that might exist
# Check platform 35, rom 35 (from the find output we saw earlier)
rom = Mock(spec=Rom)
rom.id = 35
rom.platform_id = 35
rom.fs_resources_path = "roms/35/35"
# These might exist based on the test data structure
small_exists = handler.cover_exists(rom, CoverSize.SMALL)
big_exists = handler.cover_exists(rom, CoverSize.BIG)
# We can't guarantee they exist, so just test the method works
assert isinstance(small_exists, bool)
assert isinstance(big_exists, bool)
def test_resize_cover_to_small_high_resolution(self, handler: FSResourcesHandler):
"""Test resize_cover_to_small with high resolution image"""
# Create a mock image with high resolution
mock_image = Mock()
mock_image.height = 1500
mock_image.width = 1000
mock_image.resize.return_value = mock_image
save_path = "/tmp/test_small.png"
handler.resize_cover_to_small(mock_image, save_path)
# Should use 0.2 ratio for high resolution
expected_width = int(1000 * 0.2)
expected_height = int(1500 * 0.2)
mock_image.resize.assert_called_once_with((expected_width, expected_height))
mock_image.save.assert_called_once_with(save_path)
def test_resize_cover_to_small_low_resolution(self, handler: FSResourcesHandler):
"""Test resize_cover_to_small with low resolution image"""
# Create a mock image with low resolution
mock_image = Mock()
mock_image.height = 800
mock_image.width = 600
mock_image.resize.return_value = mock_image
save_path = "/tmp/test_small.png"
handler.resize_cover_to_small(mock_image, save_path)
# Should use 0.4 ratio for low resolution
expected_width = int(600 * 0.4)
expected_height = int(800 * 0.4)
mock_image.resize.assert_called_once_with((expected_width, expected_height))
mock_image.save.assert_called_once_with(save_path)
def test_get_cover_path_no_cover(self, handler: FSResourcesHandler, rom: Rom):
"""Test _get_cover_path when no cover exists"""
result_small = handler._get_cover_path(rom, CoverSize.SMALL)
result_big = handler._get_cover_path(rom, CoverSize.BIG)
assert result_small is None
assert result_big is None
def test_get_cover_path_with_existing_cover(self, handler: FSResourcesHandler):
"""Test _get_cover_path with existing cover files"""
# Use test data that might exist
rom = Mock(spec=Rom)
rom.id = 27
rom.platform_id = 27
rom.fs_resources_path = "roms/27/27"
# Test both sizes
small_path = handler._get_cover_path(rom, CoverSize.SMALL)
big_path = handler._get_cover_path(rom, CoverSize.BIG)
# If paths exist, they should be relative to base path
if small_path:
assert not os.path.isabs(small_path)
assert "small" in small_path
if big_path:
assert not os.path.isabs(big_path)
assert "big" in big_path
@pytest.mark.asyncio
async def test_get_cover_no_entity(self, handler: FSResourcesHandler):
"""Test get_cover with no entity"""
result = await handler.get_cover(None, False, "http://example.com/cover.png")
assert result == (None, None)
@pytest.mark.asyncio
async def test_get_cover_no_url(self, handler: FSResourcesHandler, rom: Rom):
"""Test get_cover with no URL"""
result = await handler.get_cover(rom, False, None)
# Should return empty strings since no covers exist and no URL provided
assert result == (None, None)
@pytest.mark.asyncio
async def test_get_cover_with_url_no_overwrite(
self, handler: FSResourcesHandler, rom
):
"""Test get_cover with URL but no overwrite when covers don't exist"""
url = "http://example.com/cover.png"
with patch.object(handler, "_store_cover") as mock_store:
with patch.object(handler, "cover_exists") as mock_exists:
mock_exists.return_value = False
await handler.get_cover(rom, False, url)
# Should call _store_cover for both sizes since covers don't exist
assert mock_store.call_count == 2
mock_store.assert_any_call(rom, url, CoverSize.SMALL)
mock_store.assert_any_call(rom, url, CoverSize.BIG)
@pytest.mark.asyncio
async def test_get_cover_with_overwrite(
self, handler: FSResourcesHandler, rom: Rom
):
"""Test get_cover with overwrite enabled"""
url = "http://example.com/cover.png"
with patch.object(handler, "_store_cover") as mock_store:
await handler.get_cover(rom, True, url)
# Should call _store_cover for both sizes regardless of existence
assert mock_store.call_count == 2
mock_store.assert_any_call(rom, url, CoverSize.SMALL)
mock_store.assert_any_call(rom, url, CoverSize.BIG)
async def test_remove_cover_no_entity(self, handler: FSResourcesHandler):
"""Test remove_cover with no entity"""
result = await handler.remove_cover(None)
assert result == {"path_cover_s": "", "path_cover_l": ""}
async def test_remove_cover_with_entity(
self, handler: FSResourcesHandler, rom: Rom
):
"""Test remove_cover with entity"""
with patch.object(handler, "remove_directory") as mock_remove:
result = await handler.remove_cover(rom)
mock_remove.assert_called_once_with(f"{rom.fs_resources_path}/cover")
assert result == {"path_cover_s": "", "path_cover_l": ""}
async def test_build_artwork_path(self, handler: FSResourcesHandler, rom: Rom):
"""Test build_artwork_path method"""
file_ext = "png"
with patch.object(handler, "make_directory") as mock_make_dir:
with patch.object(handler, "validate_path") as mock_validate:
mock_validate.side_effect = lambda x: Path(x)
path_cover_l, path_cover_s = await handler._build_artwork_path(
rom, file_ext
)
expected_cover_path = f"{rom.fs_resources_path}/cover"
expected_big_path = f"{expected_cover_path}/big.{file_ext}"
expected_small_path = f"{expected_cover_path}/small.{file_ext}"
mock_make_dir.assert_called_once_with(expected_cover_path)
assert str(path_cover_l) == expected_big_path
assert str(path_cover_s) == expected_small_path
async def test_build_artwork_path_different_extensions(
self, handler: FSResourcesHandler, rom
):
"""Test build_artwork_path with different file extensions"""
extensions = ["png", "jpg", "jpeg", "gif", "webp"]
for ext in extensions:
with patch.object(handler, "make_directory"):
with patch.object(handler, "validate_path") as mock_validate:
mock_validate.side_effect = lambda x: Path(x)
path_cover_l, path_cover_s = await handler._build_artwork_path(
rom, ext
)
# Check that the extension is properly included
assert str(path_cover_l).endswith(f"big.{ext}")
assert str(path_cover_s).endswith(f"small.{ext}")
def test_get_screenshot_path(self, handler: FSResourcesHandler, rom: Rom):
"""Test _get_screenshot_path method"""
idx = "0"
result = handler._get_screenshot_path(rom, idx)
expected = f"{rom.fs_resources_path}/screenshots/{idx}.jpg"
assert result == expected
def test_get_screenshot_path_different_indices(
self, handler: FSResourcesHandler, rom
):
"""Test _get_screenshot_path with different indices"""
indices = ["0", "1", "2", "10", "99"]
for idx in indices:
result = handler._get_screenshot_path(rom, idx)
expected = f"{rom.fs_resources_path}/screenshots/{idx}.jpg"
assert result == expected
@pytest.mark.asyncio
async def test_get_rom_screenshots_no_rom(self, handler: FSResourcesHandler):
"""Test get_rom_screenshots with no ROM"""
result = await handler.get_rom_screenshots(
None, ["http://example.com/screenshot.jpg"]
)
assert result == []
@pytest.mark.asyncio
async def test_get_rom_screenshots_no_urls(
self, handler: FSResourcesHandler, rom: Rom
):
"""Test get_rom_screenshots with no URLs"""
result = await handler.get_rom_screenshots(rom, None)
assert result == []
result = await handler.get_rom_screenshots(rom, [])
assert result == []
@pytest.mark.asyncio
async def test_get_rom_screenshots_with_urls(
self, handler: FSResourcesHandler, rom
):
"""Test get_rom_screenshots with URLs"""
urls = [
"http://example.com/screenshot1.jpg",
"http://example.com/screenshot2.jpg",
]
with patch.object(handler, "_store_screenshot") as mock_store:
result = await handler.get_rom_screenshots(rom, urls)
# Should call _store_screenshot for each URL
assert mock_store.call_count == 2
mock_store.assert_any_call(rom, urls[0], 0)
mock_store.assert_any_call(rom, urls[1], 1)
# Should return paths for each screenshot
expected_paths = [
f"{rom.fs_resources_path}/screenshots/0.jpg",
f"{rom.fs_resources_path}/screenshots/1.jpg",
]
assert result == expected_paths
def test_manual_exists_no_manual(self, handler: FSResourcesHandler, rom: Rom):
"""Test manual_exists when no manual exists"""
assert not handler.manual_exists(rom)
def test_get_manual_path_no_manual(self, handler: FSResourcesHandler, rom: Rom):
"""Test _get_manual_path when no manual exists"""
result = handler._get_manual_path(rom)
assert result is None
@pytest.mark.asyncio
async def test_get_manual_no_rom(self, handler: FSResourcesHandler):
"""Test get_manual with no ROM"""
result = await handler.get_manual(None, False, "http://example.com/manual.pdf")
assert result is None
@pytest.mark.asyncio
async def test_get_manual_no_url(self, handler: FSResourcesHandler, rom: Rom):
"""Test get_manual with no URL"""
result = await handler.get_manual(rom, False, None)
assert result is None
@pytest.mark.asyncio
async def test_get_manual_with_url_no_overwrite(
self, handler: FSResourcesHandler, rom
):
"""Test get_manual with URL but no overwrite when manual doesn't exist"""
url = "http://example.com/manual.pdf"
with patch.object(handler, "_store_manual") as mock_store:
with patch.object(handler, "manual_exists") as mock_exists:
mock_exists.return_value = False
await handler.get_manual(rom, False, url)
# Should call _store_manual since manual doesn't exist
mock_store.assert_called_once_with(rom, url)
@pytest.mark.asyncio
async def test_get_manual_with_overwrite(
self, handler: FSResourcesHandler, rom: Rom
):
"""Test get_manual with overwrite enabled"""
url = "http://example.com/manual.pdf"
with patch.object(handler, "_store_manual") as mock_store:
await handler.get_manual(rom, True, url)
# Should call _store_manual regardless of existence
mock_store.assert_called_once_with(rom, url)
def test_get_ra_resources_path(self, handler: FSResourcesHandler):
"""Test get_ra_resources_path method"""
platform_id = 1
rom_id = 42
result = handler.get_ra_resources_path(platform_id, rom_id)
expected = os.path.join("roms", "1", "42", "retroachievements")
assert result == expected
def test_get_ra_resources_path_different_ids(self, handler: FSResourcesHandler):
"""Test get_ra_resources_path with different IDs"""
test_cases = [(1, 1), (42, 123), (999, 456), (10, 999)]
for platform_id, rom_id in test_cases:
result = handler.get_ra_resources_path(platform_id, rom_id)
expected = os.path.join(
"roms", str(platform_id), str(rom_id), "retroachievements"
)
assert result == expected
def test_get_ra_badges_path(self, handler: FSResourcesHandler):
"""Test get_ra_badges_path method"""
platform_id = 1
rom_id = 42
result = handler.get_ra_badges_path(platform_id, rom_id)
expected = os.path.join("roms", "1", "42", "retroachievements", "badges")
assert result == expected
def test_get_ra_badges_path_different_ids(self, handler: FSResourcesHandler):
"""Test get_ra_badges_path with different IDs"""
test_cases = [(1, 1), (42, 123), (999, 456), (10, 999)]
for platform_id, rom_id in test_cases:
result = handler.get_ra_badges_path(platform_id, rom_id)
expected = os.path.join(
"roms", str(platform_id), str(rom_id), "retroachievements", "badges"
)
assert result == expected
async def test_create_ra_resources_path(self, handler: FSResourcesHandler):
"""Test create_ra_resources_path method"""
platform_id = 1
rom_id = 42
with patch.object(handler, "make_directory") as mock_make_dir:
await handler.create_ra_resources_path(platform_id, rom_id)
expected_path = handler.get_ra_resources_path(platform_id, rom_id)
mock_make_dir.assert_called_once_with(expected_path)
def test_integration_with_base_handler_methods(self, handler: FSResourcesHandler):
"""Test that FSResourcesHandler properly inherits from FSHandler"""
# Test that handler has base methods
assert hasattr(handler, "validate_path")
assert hasattr(handler, "make_directory")
assert hasattr(handler, "remove_directory")
assert hasattr(handler, "write_file_streamed")
assert hasattr(handler, "file_exists")
assert hasattr(handler, "stream_file")
def test_cover_size_enum_values(self, handler: FSResourcesHandler, rom: Rom):
"""Test that cover size enum values are handled correctly"""
# Test that the enum values are used correctly in path construction
with patch.object(handler, "validate_path") as mock_validate:
mock_validate.return_value = Path("test_path")
# Test SMALL size
handler._get_cover_path(rom, CoverSize.SMALL)
# Should look for files with "small" in the name
call_args = mock_validate.call_args[0][0]
assert "cover" in call_args
# Test BIG size
handler._get_cover_path(rom, CoverSize.BIG)
call_args = mock_validate.call_args[0][0]
assert "cover" in call_args
def test_path_construction_consistency(self, handler: FSResourcesHandler):
"""Test that path construction is consistent across methods"""
platform_id = 1
rom_id = 42
# Test platform resources path
platform_path = handler.get_platform_resources_path(platform_id)
assert platform_path.endswith(f"roms/{platform_id}")
# Test RA base path
ra_base = handler.get_ra_resources_path(platform_id, rom_id)
assert ra_base.startswith(f"roms/{platform_id}/{rom_id}")
# Test RA badges path
ra_badges = handler.get_ra_badges_path(platform_id, rom_id)
assert ra_badges.startswith(f"roms/{platform_id}/{rom_id}")
assert ra_badges.endswith("badges")
def test_entity_types_handling(self, rom, handler: FSResourcesHandler):
"""Test that both Rom and Collection entities are handled correctly"""
collection = Mock(spec=Collection)
collection.id = 1
collection.fs_resources_path = "collections/1"
# Both should work with cover_exists
rom_result = handler.cover_exists(rom, CoverSize.SMALL)
collection_result = handler.cover_exists(collection, CoverSize.SMALL)
assert isinstance(rom_result, bool)
assert isinstance(collection_result, bool)
def test_error_handling_edge_cases(self, handler: FSResourcesHandler):
"""Test error handling for edge cases"""
# Test with invalid path
invalid_entity = Mock()
invalid_entity.fs_resources_path = ""
# Should raise a ValueError
with pytest.raises(ValueError):
handler.cover_exists(invalid_entity, CoverSize.SMALL)
def test_existing_resources_structure(self, handler: FSResourcesHandler):
"""Test with the existing resources structure in romm_test"""
# Test that the handler can work with the existing directory structure
# without creating new files
# Test platform resources path with existing structure
platform_path = handler.get_platform_resources_path(35)
assert isinstance(platform_path, str)
assert "roms" in platform_path
assert "35" in platform_path
# Test RA paths with existing structure
ra_base = handler.get_ra_resources_path(35, 35)
ra_badges = handler.get_ra_badges_path(35, 35)
assert isinstance(ra_base, str)
assert isinstance(ra_badges, str)
assert "retroachievements" in ra_base
assert "badges" in ra_badges