rommapp_romm/backend/tests/tasks/test_sync_retroachievements_progress.py
Georges-Antoine Assi 72e884a83c
run fmt
2026-03-12 19:02:24 -04:00

327 lines
12 KiB
Python

from unittest.mock import MagicMock
import pytest
from adapters.services.retroachievements_types import RAUserCompletionProgressKind
from handler.database.roms_handler import DBRomsHandler
from handler.database.users_handler import DBUsersHandler
from handler.metadata.ra_handler import RAHandler
from models.rom import RomUser, RomUserStatus
from tasks.scheduled.sync_retroachievements_progress import (
SyncRetroAchievementsProgressTask,
_get_rom_user_status_from_ra_award_kind,
)
@pytest.fixture
def task() -> SyncRetroAchievementsProgressTask:
"""Create a task instance for testing."""
return SyncRetroAchievementsProgressTask()
class TestGetRomUserStatusFromRaAwardKind:
"""Tests for the _get_rom_user_status_from_ra_award_kind helper."""
def test_mastered_returns_completed_100(self):
assert (
_get_rom_user_status_from_ra_award_kind(
RAUserCompletionProgressKind.MASTERED
)
== RomUserStatus.COMPLETED_100
)
def test_completed_returns_completed_100(self):
assert (
_get_rom_user_status_from_ra_award_kind(
RAUserCompletionProgressKind.COMPLETED
)
== RomUserStatus.COMPLETED_100
)
def test_beaten_hardcore_returns_finished(self):
assert (
_get_rom_user_status_from_ra_award_kind(
RAUserCompletionProgressKind.BEATEN_HARDCORE
)
== RomUserStatus.FINISHED
)
def test_beaten_softcore_returns_finished(self):
assert (
_get_rom_user_status_from_ra_award_kind(
RAUserCompletionProgressKind.BEATEN_SOFTCORE
)
== RomUserStatus.FINISHED
)
def test_none_returns_incomplete(self):
assert _get_rom_user_status_from_ra_award_kind(None) == RomUserStatus.INCOMPLETE
def test_unknown_value_returns_none(self):
assert _get_rom_user_status_from_ra_award_kind("unknown_award") is None
class TestSyncRetroAchievementsProgressTask:
"""Test suite for SyncRetroAchievementsProgressTask."""
def test_task_initialization(self, task):
"""Test task initialization with correct parameters."""
assert (
task.func
== "tasks.scheduled.sync_retroachievements_progress.sync_retroachievements_progress_task.run"
)
assert task.description == "Updates RetroAchievements progress for all users"
async def test_run_when_retroachievements_api_disabled(self, task, mocker):
"""Test run method when RetroAchievements API is disabled."""
mocker.patch.object(RAHandler, "is_enabled", return_value=False)
mock_log = mocker.patch("tasks.scheduled.sync_retroachievements_progress.log")
await task.run()
mock_log.warning.assert_called_once_with(
"RetroAchievements API is not enabled, skipping progress sync"
)
async def test_run_when_no_users_set(self, task, mocker):
"""Test run method when no users have RetroAchievements usernames set"""
mock_get_users = mocker.patch.object(
DBUsersHandler, "get_users", return_value=[]
)
mock_get_user_progression = mocker.patch.object(
RAHandler, "get_user_progression"
)
await task.run()
mock_get_users.assert_called_once_with(has_ra_username=True)
mock_get_user_progression.assert_not_called()
async def test_run_saves_progress(self, task, viewer_user, mocker):
"""Test run method saves retrieved progress."""
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mock_update_user = mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {"total": 0, "results": []}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
await task.run()
mock_update_user.assert_called_once_with(
viewer_user.id,
{"ra_progression": user_progression},
)
async def test_run_is_resilient_to_errors(
self, task, viewer_user, editor_user, mocker
):
"""Test run method saves retrieved progress for a user even if another user fails."""
mocker.patch.object(
DBUsersHandler, "get_users", return_value=[viewer_user, editor_user]
)
user_progression = {"total": 0, "results": []}
mocker.patch.object(
RAHandler,
"get_user_progression",
side_effect=[
# Call for first user raises an exception.
Exception("API error"),
# Call for second user returns valid progression.
user_progression,
],
)
mock_update_user = mocker.patch.object(DBUsersHandler, "update_user")
await task.run()
mock_update_user.assert_called_once_with(
editor_user.id,
{"ra_progression": user_progression},
)
async def test_run_sets_rom_user_status_when_unset(
self, task, viewer_user, rom, mocker
):
"""Test that rom_user.status is set from RA award kind when currently unset."""
ra_id = 12345
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {
"total": 1,
"results": [
{
"rom_ra_id": ra_id,
"max_possible": 50,
"num_awarded": 50,
"num_awarded_hardcore": 50,
"highest_award_kind": RAUserCompletionProgressKind.MASTERED,
"earned_achievements": [],
}
],
}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
mock_rom = MagicMock()
mock_rom.id = rom.id
mocker.patch.object(
DBRomsHandler, "get_rom_by_metadata_id", return_value=mock_rom
)
mock_rom_user = MagicMock(spec=RomUser)
mock_rom_user.id = 1
mock_rom_user.status = None
mocker.patch.object(DBRomsHandler, "get_rom_user", return_value=mock_rom_user)
mock_update_rom_user = mocker.patch.object(DBRomsHandler, "update_rom_user")
await task.run()
mock_update_rom_user.assert_called_once_with(
mock_rom_user.id, {"status": RomUserStatus.COMPLETED_100}
)
async def test_run_updates_existing_rom_user_status_when_changed(
self, task, viewer_user, rom, mocker
):
"""Test that rom_user.status is updated when the RA award kind changes."""
ra_id = 12345
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {
"total": 1,
"results": [
{
"rom_ra_id": ra_id,
"max_possible": 50,
"num_awarded": 50,
"num_awarded_hardcore": 50,
"highest_award_kind": RAUserCompletionProgressKind.MASTERED,
"earned_achievements": [],
}
],
}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
mock_rom = MagicMock()
mock_rom.id = rom.id
mocker.patch.object(
DBRomsHandler, "get_rom_by_metadata_id", return_value=mock_rom
)
mock_rom_user = MagicMock(spec=RomUser)
mock_rom_user.id = 1
# Previously INCOMPLETE (set by an earlier sync), now mastered in RA
mock_rom_user.status = RomUserStatus.INCOMPLETE
mocker.patch.object(DBRomsHandler, "get_rom_user", return_value=mock_rom_user)
mock_update_rom_user = mocker.patch.object(DBRomsHandler, "update_rom_user")
await task.run()
mock_update_rom_user.assert_called_once_with(
mock_rom_user.id, {"status": RomUserStatus.COMPLETED_100}
)
async def test_run_skips_update_when_status_already_matches(
self, task, viewer_user, rom, mocker
):
"""Test that rom_user.status is not written again when it already matches RA."""
ra_id = 12345
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {
"total": 1,
"results": [
{
"rom_ra_id": ra_id,
"max_possible": 50,
"num_awarded": 50,
"num_awarded_hardcore": 50,
"highest_award_kind": RAUserCompletionProgressKind.MASTERED,
"earned_achievements": [],
}
],
}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
mock_rom = MagicMock()
mock_rom.id = rom.id
mocker.patch.object(
DBRomsHandler, "get_rom_by_metadata_id", return_value=mock_rom
)
mock_rom_user = MagicMock(spec=RomUser)
mock_rom_user.id = 1
mock_rom_user.status = RomUserStatus.COMPLETED_100 # Already up-to-date
mocker.patch.object(DBRomsHandler, "get_rom_user", return_value=mock_rom_user)
mock_update_rom_user = mocker.patch.object(DBRomsHandler, "update_rom_user")
await task.run()
mock_update_rom_user.assert_not_called()
async def test_run_sets_incomplete_status_for_started_games(
self, task, viewer_user, rom, mocker
):
"""Test that INCOMPLETE status is set for games started but not beaten in RA."""
ra_id = 99999
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {
"total": 1,
"results": [
{
"rom_ra_id": ra_id,
"max_possible": 100,
"num_awarded": 10,
"num_awarded_hardcore": 0,
"highest_award_kind": None,
"earned_achievements": [],
}
],
}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
mock_rom = MagicMock()
mock_rom.id = rom.id
mocker.patch.object(
DBRomsHandler, "get_rom_by_metadata_id", return_value=mock_rom
)
mock_rom_user = MagicMock(spec=RomUser)
mock_rom_user.id = 1
mock_rom_user.status = None
mocker.patch.object(DBRomsHandler, "get_rom_user", return_value=mock_rom_user)
mock_update_rom_user = mocker.patch.object(DBRomsHandler, "update_rom_user")
await task.run()
mock_update_rom_user.assert_called_once_with(
mock_rom_user.id, {"status": RomUserStatus.INCOMPLETE}
)
async def test_run_skips_status_update_when_rom_not_found(
self, task, viewer_user, mocker
):
"""Test that status update is skipped when the ROM is not in the database."""
mocker.patch.object(DBUsersHandler, "get_users", return_value=[viewer_user])
mocker.patch.object(DBUsersHandler, "update_user")
user_progression = {
"total": 1,
"results": [
{
"rom_ra_id": 99999,
"highest_award_kind": RAUserCompletionProgressKind.MASTERED,
"earned_achievements": [],
}
],
}
mocker.patch.object(
RAHandler, "get_user_progression", return_value=user_progression
)
mocker.patch.object(DBRomsHandler, "get_rom_by_metadata_id", return_value=None)
mock_update_rom_user = mocker.patch.object(DBRomsHandler, "update_rom_user")
await task.run()
mock_update_rom_user.assert_not_called()