complete tests for tasks folder

This commit is contained in:
Georges-Antoine Assi
2025-07-05 23:10:47 -04:00
parent 19245ca158
commit 48c355fa94
10 changed files with 437 additions and 118 deletions

View File

@@ -1,5 +1,4 @@
import zipfile
from io import BytesIO
import os
import pytest
from tasks.update_launchbox_metadata import UpdateLaunchboxMetadataTask
@@ -13,93 +12,11 @@ def task():
@pytest.fixture
def sample_zip_content():
"""Create sample ZIP content with XML files"""
zip_buffer = BytesIO()
test_dir = os.path.dirname(__file__)
sample_path = os.path.join(test_dir, "fixtures", "sample_metadata.zip")
with zipfile.ZipFile(zip_buffer, "w") as zip_file:
# Add Platforms.xml
platforms_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Platform>
<Name>Nintendo 64</Name>
<PlatformType>Console</PlatformType>
<ReleaseDate>1996-06-23</ReleaseDate>
</Platform>
<Platform>
<Name>PlayStation</Name>
<PlatformType>Console</PlatformType>
<ReleaseDate>1994-12-03</ReleaseDate>
</Platform>
</LaunchBox>"""
zip_file.writestr("Platforms.xml", platforms_xml)
# Add Metadata.xml
metadata_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Game>
<DatabaseID>67890</DatabaseID>
<Name>Crash Bandicoot</Name>
<Platform>PlayStation</Platform>
<ReleaseDate>1996-09-09</ReleaseDate>
<Genre>Platformer</Genre>
</Game>
<Game>
<DatabaseID>12345</DatabaseID>
<Name>Super Mario 64</Name>
<Platform>Nintendo 64</Platform>
<ReleaseDate>1996-06-23</ReleaseDate>
<Genre>Platformer</Genre>
</Game>
<GameAlternateName>
<AlternateName>Super Mario 64 (USA)</AlternateName>
<DatabaseID>12345</DatabaseID>
</GameAlternateName>
<GameImage>
<DatabaseID>12345</DatabaseID>
<FileName>super_mario_64.jpg</FileName>
<Type>Cover</Type>
</GameImage>
<GameImage>
<DatabaseID>12345</DatabaseID>
<FileName>super_mario_64_screenshot.jpg</FileName>
<Type>Screenshot</Type>
</GameImage>
</LaunchBox>"""
zip_file.writestr("Metadata.xml", metadata_xml)
# Add Mame.xml
mame_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<MameFile>
<FileName>mario.zip</FileName>
<GameName>mario</GameName>
<Description>Super Mario Bros.</Description>
</MameFile>
<MameFile>
<FileName>pacman.zip</FileName>
<GameName>pacman</GameName>
<Description>Pac-Man</Description>
</MameFile>
</LaunchBox>"""
zip_file.writestr("Mame.xml", mame_xml)
# Add Files.xml
files_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<File>
<FileName>super_mario_64.z64</FileName>
<FileType>ROM</FileType>
<Size>8388608</Size>
</File>
<File>
<FileName>crash_bandicoot.bin</FileName>
<FileType>ROM</FileType>
<Size>524288000</Size>
</File>
</LaunchBox>"""
zip_file.writestr("Files.xml", files_xml)
return zip_buffer.getvalue()
with open(sample_path, "rb") as f:
return f.read()
@pytest.fixture

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Platform>
<Name></Name>
<PlatformType>Console</PlatformType>
</Platform>
<Platform>
<Name>Valid Platform</Name>
<PlatformType></PlatformType>
</Platform>
</LaunchBox>

Binary file not shown.

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<File>
<FileName>super_mario_64.z64</FileName>
<FileType>ROM</FileType>
<Size>8388608</Size>
</File>
<File>
<FileName>crash_bandicoot.bin</FileName>
<FileType>ROM</FileType>
<Size>524288000</Size>
</File>
</LaunchBox>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<MameFile>
<FileName>mario.zip</FileName>
<GameName>mario</GameName>
<Description>Super Mario Bros.</Description>
</MameFile>
<MameFile>
<FileName>pacman.zip</FileName>
<GameName>pacman</GameName>
<Description>Pac-Man</Description>
</MameFile>
</LaunchBox>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Game>
<DatabaseID>67890</DatabaseID>
<Name>Crash Bandicoot</Name>
<Platform>PlayStation</Platform>
<ReleaseDate>1996-09-09</ReleaseDate>
<Genre>Platformer</Genre>
</Game>
<Game>
<DatabaseID>12345</DatabaseID>
<Name>Super Mario 64</Name>
<Platform>Nintendo 64</Platform>
<ReleaseDate>1996-06-23</ReleaseDate>
<Genre>Platformer</Genre>
</Game>
<GameAlternateName>
<AlternateName>Super Mario 64 (USA)</AlternateName>
<DatabaseID>12345</DatabaseID>
</GameAlternateName>
<GameImage>
<DatabaseID>12345</DatabaseID>
<FileName>super_mario_64.jpg</FileName>
<Type>Cover</Type>
</GameImage>
<GameImage>
<DatabaseID>12345</DatabaseID>
<FileName>super_mario_64_screenshot.jpg</FileName>
<Type>Screenshot</Type>
</GameImage>
</LaunchBox>

View File

@@ -0,0 +1,50 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from handler.scan_handler import ScanType
from tasks.scan_library import ScanLibraryTask, scan_library_task
from tasks.tasks import PeriodicTask
class TestScanLibraryTask:
@pytest.fixture
def task(self):
return ScanLibraryTask()
def test_init(self, task):
"""Test task initialization"""
assert task.func == "tasks.scan_library.scan_library_task.run"
assert task.description == "library scan"
@patch("tasks.scan_library.ENABLE_SCHEDULED_RESCAN", True)
@patch("tasks.scan_library.scan_platforms")
@patch("tasks.scan_library.log")
async def test_run_enabled(self, mock_log, mock_scan_platforms, task):
"""Test run when scheduled rescan is enabled"""
mock_scan_platforms.return_value = AsyncMock()
await task.run()
mock_log.info.assert_any_call("Scheduled library scan started...")
mock_scan_platforms.assert_called_once_with([], scan_type=ScanType.UNIDENTIFIED)
mock_log.info.assert_any_call("Scheduled library scan done")
@patch("tasks.scan_library.ENABLE_SCHEDULED_RESCAN", False)
@patch("tasks.scan_library.scan_platforms")
@patch("tasks.scan_library.log")
async def test_run_disabled(self, mock_log, mock_scan_platforms, task):
"""Test run when scheduled rescan is disabled"""
task.unschedule = MagicMock()
await task.run()
mock_log.info.assert_called_once_with(
"Scheduled library scan not enabled, unscheduling..."
)
task.unschedule.assert_called_once()
mock_scan_platforms.assert_not_called()
def test_task_instance(self):
"""Test that the module-level task instance is created correctly"""
assert isinstance(scan_library_task, ScanLibraryTask)
assert scan_library_task.func == "tasks.scan_library.scan_library_task.run"

View File

@@ -0,0 +1,300 @@
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from exceptions.task_exceptions import SchedulerException
from rq.job import Job
from tasks.tasks import PeriodicTask, RemoteFilePullTask, tasks_scheduler
class ConcretePeriodicTask(PeriodicTask):
"""Concrete implementation for testing abstract PeriodicTask"""
async def run(self, *args, **kwargs):
return "test_result"
class TestPeriodicTask:
@pytest.fixture
def task(self):
return ConcretePeriodicTask(
func="test.function",
description="test task",
enabled=True,
cron_string="0 0 * * *",
)
@pytest.fixture
def disabled_task(self):
return ConcretePeriodicTask(
func="test.disabled.function",
description="disabled task",
enabled=False,
cron_string="0 0 * * *",
)
def test_init(self, task):
"""Test task initialization"""
assert task.func == "test.function"
assert task.description == "test task"
assert task.enabled is True
assert task.cron_string == "0 0 * * *"
def test_init_default_description(self):
"""Test that description defaults to func when not provided"""
task = ConcretePeriodicTask(func="test.function", description=None)
assert task.description == "test.function"
@patch.object(tasks_scheduler, "get_jobs")
def test_get_existing_job_found(self, mock_get_jobs, task):
"""Test finding an existing job"""
mock_job = MagicMock(spec=Job)
mock_job.func_name = "test.function"
mock_get_jobs.return_value = [mock_job]
result = task._get_existing_job()
assert result == mock_job
@patch.object(tasks_scheduler, "get_jobs")
def test_get_existing_job_not_found(self, mock_get_jobs, task):
"""Test when no existing job is found"""
mock_job = MagicMock(spec=Job)
mock_job.func_name = "other.function"
mock_get_jobs.return_value = [mock_job]
result = task._get_existing_job()
assert result is None
@patch.object(tasks_scheduler, "get_jobs")
def test_get_existing_job_empty_list(self, mock_get_jobs, task):
"""Test when no jobs exist"""
mock_get_jobs.return_value = []
result = task._get_existing_job()
assert result is None
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch.object(ConcretePeriodicTask, "schedule")
def test_init_enabled_no_existing_job(
self, mock_schedule, mock_get_existing_job, task
):
"""Test init when task is enabled and no existing job"""
mock_get_existing_job.return_value = None
mock_schedule.return_value = "scheduled"
result = task.init()
mock_schedule.assert_called_once()
assert result == "scheduled"
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch.object(ConcretePeriodicTask, "unschedule")
def test_init_disabled_with_existing_job(
self, mock_unschedule, mock_get_existing_job, disabled_task
):
"""Test init when task is disabled but has existing job"""
mock_job = MagicMock()
mock_get_existing_job.return_value = mock_job
mock_unschedule.return_value = "unscheduled"
result = disabled_task.init()
mock_unschedule.assert_called_once()
assert result == "unscheduled"
@patch.object(ConcretePeriodicTask, "_get_existing_job")
def test_init_enabled_with_existing_job(self, mock_get_existing_job, task):
"""Test init when task is enabled and job already exists"""
mock_job = MagicMock()
mock_get_existing_job.return_value = mock_job
result = task.init()
assert result is None # Should do nothing
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch.object(tasks_scheduler, "cron")
def test_schedule_success(self, mock_cron, mock_get_existing_job, task):
"""Test successful scheduling"""
mock_get_existing_job.return_value = None
mock_cron.return_value = "scheduled_job"
result = task.schedule()
mock_cron.assert_called_once_with(
"0 0 * * *", func="test.function", repeat=None
)
assert result == "scheduled_job"
def test_schedule_not_enabled(self, disabled_task):
"""Test scheduling when task is not enabled"""
with pytest.raises(
SchedulerException, match="Scheduled disabled task is not enabled."
):
disabled_task.schedule()
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch("tasks.tasks.log")
def test_schedule_already_scheduled(self, mock_log, mock_get_existing_job, task):
"""Test scheduling when job already exists"""
mock_job = MagicMock()
mock_get_existing_job.return_value = mock_job
result = task.schedule()
mock_log.info.assert_called_once_with("Test task is already scheduled.")
assert result is None
def test_schedule_no_cron_string(self):
"""Test scheduling with no cron string"""
task = ConcretePeriodicTask(
func="test.function",
description="test task",
enabled=True,
cron_string=None,
)
with patch.object(task, "_get_existing_job", return_value=None):
result = task.schedule()
assert result is None
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch.object(tasks_scheduler, "cancel")
@patch("tasks.tasks.log")
def test_unschedule_success(
self, mock_log, mock_cancel, mock_get_existing_job, task
):
"""Test successful unscheduling"""
mock_job = MagicMock()
mock_get_existing_job.return_value = mock_job
task.unschedule()
mock_cancel.assert_called_once_with(mock_job)
mock_log.info.assert_called_once_with("Test task unscheduled.")
@patch.object(ConcretePeriodicTask, "_get_existing_job")
@patch("tasks.tasks.log")
def test_unschedule_not_scheduled(self, mock_log, mock_get_existing_job, task):
"""Test unscheduling when no job exists"""
mock_get_existing_job.return_value = None
task.unschedule()
mock_log.info.assert_called_once_with("Test task is not scheduled.")
async def test_run_abstract_method(self, task):
"""Test that run method works in concrete implementation"""
result = await task.run()
assert result == "test_result"
class TestRemoteFilePullTask:
@pytest.fixture
def task(self):
return RemoteFilePullTask(
func="test.remote.function",
description="remote test task",
enabled=True,
cron_string="0 0 * * *",
url="https://example.com/data.json",
)
@pytest.fixture
def disabled_task(self):
return RemoteFilePullTask(
func="test.remote.disabled.function",
description="disabled remote task",
enabled=False,
url="https://example.com/data.json",
)
def test_init(self, task):
"""Test RemoteFilePullTask initialization"""
assert task.func == "test.remote.function"
assert task.description == "remote test task"
assert task.enabled is True
assert task.url == "https://example.com/data.json"
@patch("tasks.tasks.ctx_httpx_client")
@patch("tasks.tasks.log")
async def test_run_success(self, mock_log, mock_ctx_httpx_client, task):
"""Test successful remote file pull"""
mock_client = AsyncMock()
mock_response = AsyncMock()
mock_response.content = b"test content"
mock_client.get.return_value = mock_response
mock_ctx_httpx_client.get.return_value = mock_client
result = await task.run(force=True)
mock_client.get.assert_called_once_with(
"https://example.com/data.json", timeout=120
)
mock_response.raise_for_status.assert_called_once()
mock_log.info.assert_called_once_with("Scheduled remote test task started...")
assert result == b"test content"
@patch("tasks.tasks.ctx_httpx_client")
@patch("tasks.tasks.log")
async def test_run_http_error(self, mock_log, mock_ctx_httpx_client, task):
"""Test handling of HTTP errors"""
mock_client = AsyncMock()
mock_client.get.side_effect = httpx.HTTPError("Connection failed")
mock_ctx_httpx_client.get.return_value = mock_client
result = await task.run(force=True)
mock_log.error.assert_called()
assert result is None
@patch("tasks.tasks.ctx_httpx_client")
@patch("tasks.tasks.log")
async def test_run_response_error(self, mock_log, mock_ctx_httpx_client, task):
"""Test handling of response status errors"""
mock_client = AsyncMock()
mock_response = MagicMock()
# Create a proper HTTPStatusError
http_error = httpx.HTTPStatusError(
"404 Not Found", request=MagicMock(), response=MagicMock()
)
mock_response.raise_for_status.side_effect = http_error
mock_client.get.return_value = mock_response
mock_ctx_httpx_client.get.return_value = mock_client
result = await task.run(force=True)
# Verify the specific error logging calls
mock_log.error.assert_any_call(
"Scheduled remote test task failed", exc_info=True
)
mock_log.error.assert_any_call(http_error)
assert result is None
@patch.object(RemoteFilePullTask, "unschedule")
@patch("tasks.tasks.log")
async def test_run_disabled_not_forced(
self, mock_log, mock_unschedule, disabled_task
):
"""Test run when task is disabled and not forced"""
result = await disabled_task.run(force=False)
mock_log.info.assert_called_once_with(
"Scheduled disabled remote task not enabled, unscheduling..."
)
mock_unschedule.assert_called_once()
assert result is None
@patch("tasks.tasks.ctx_httpx_client")
async def test_run_disabled_but_forced(self, mock_ctx_httpx_client, disabled_task):
"""Test run when task is disabled but forced"""
mock_client = AsyncMock()
mock_response = AsyncMock()
mock_response.content = b"forced content"
mock_client.get.return_value = mock_response
mock_ctx_httpx_client.get.return_value = mock_client
result = await disabled_task.run(force=True)
assert result == b"forced content"

View File

@@ -1,7 +1,7 @@
import zipfile
from io import BytesIO
import os
from unittest.mock import AsyncMock, patch
import anyio
import pytest
from tasks.tasks import RemoteFilePullTask
from tasks.update_launchbox_metadata import (
@@ -165,23 +165,13 @@ class TestUpdateLaunchboxMetadataTask:
task,
):
"""Test handling of XML elements with empty or missing text"""
# Create XML with empty elements
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zip_file:
platforms_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Platform>
<Name></Name>
<PlatformType>Console</PlatformType>
</Platform>
<Platform>
<Name>Valid Platform</Name>
<PlatformType></PlatformType>
</Platform>
</LaunchBox>"""
zip_file.writestr("Platforms.xml", platforms_xml)
test_dir = os.path.dirname(__file__)
sample_path = os.path.join(
test_dir, "fixtures", "sample_metadata_with_empty_elements.zip"
)
mock_super_run.return_value = zip_buffer.getvalue()
async with await anyio.open_file(sample_path, "rb") as f:
mock_super_run.return_value = await f.read()
# Create a mock pipeline with async context manager support
mock_pipe = AsyncMock()
@@ -214,19 +204,13 @@ class TestUpdateLaunchboxMetadataTask:
task,
):
"""Test handling when some XML files are missing from the ZIP"""
# Create ZIP with only Platforms.xml
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zip_file:
platforms_xml = """<?xml version="1.0" encoding="UTF-8"?>
<LaunchBox>
<Platform>
<Name>Test Platform</Name>
<PlatformType>Console</PlatformType>
</Platform>
</LaunchBox>"""
zip_file.writestr("Platforms.xml", platforms_xml)
test_dir = os.path.dirname(__file__)
sample_path = os.path.join(
test_dir, "fixtures", "sample_metadata_with_empty_elements.zip"
)
mock_super_run.return_value = zip_buffer.getvalue()
async with await anyio.open_file(sample_path, "rb") as f:
mock_super_run.return_value = await f.read()
# Create a mock pipeline with async context manager support
mock_pipe = AsyncMock()