mirror of
https://github.com/rommapp/romm.git
synced 2026-06-30 15:55:54 +00:00
IGDB catalogues a console and its regional twin as two separate platforms (SNES/Super Famicom, NES/Famicom). RomM locked each IGDB search to a single platform id, so a region-exclusive title catalogued under only the twin — e.g. the Japan-only Super Famicom game "Rudra no Hihou" (platform 58) scanned from an `snes` folder (platform 19) — was filtered out before name matching ran and never matched. Include a platform's regional twin in the IGDB platform filter so both are searched. A non-twin platform keeps the exact existing query (`platforms=[19]`); a twin produces an OR group (`(platforms=[19] | platforms=[58])`), leaving all other platforms and recorded cassettes unchanged. Written primarily by Claude Code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
660 lines
23 KiB
Python
660 lines
23 KiB
Python
"""Tests for the IGDB metadata handler."""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from adapters.services.igdb_types import GameType
|
|
from handler.metadata.igdb_handler import (
|
|
FAMICOM_IGDB_ID,
|
|
NES_IGDB_ID,
|
|
SNES_IGDB_ID,
|
|
SUPER_FAMICOM_IGDB_ID,
|
|
IGDBHandler,
|
|
_build_platforms_where,
|
|
_platform_igdb_ids_with_twin,
|
|
)
|
|
|
|
GENESIS_IGDB_ID = 29
|
|
|
|
|
|
def _make_game(
|
|
game_id: int,
|
|
name: str,
|
|
alternative_names: list[str] | None = None,
|
|
game_localizations: list[str] | None = None,
|
|
) -> dict:
|
|
"""Build a minimal IGDB Game dict for testing.
|
|
|
|
``alternative_names`` and ``game_localizations`` accept plain title strings
|
|
and are wrapped into the ``{"name": ...}`` shape IGDB returns.
|
|
"""
|
|
return {
|
|
"id": game_id,
|
|
"name": name,
|
|
"slug": name.lower().replace(" ", "-"),
|
|
"summary": "",
|
|
"total_rating": 0.0,
|
|
"aggregated_rating": 0.0,
|
|
"first_release_date": None,
|
|
"artworks": [],
|
|
"cover": None,
|
|
"screenshots": [],
|
|
"platforms": [{"id": GENESIS_IGDB_ID, "name": "Sega Mega Drive/Genesis"}],
|
|
"alternative_names": [{"name": n} for n in (alternative_names or [])],
|
|
"genres": [],
|
|
"franchise": None,
|
|
"franchises": [],
|
|
"collections": [],
|
|
"game_modes": [],
|
|
"involved_companies": [],
|
|
"expansions": [],
|
|
"dlcs": [],
|
|
"remasters": [],
|
|
"remakes": [],
|
|
"expanded_games": [],
|
|
"ports": [],
|
|
"similar_games": [],
|
|
"videos": [],
|
|
"age_ratings": [],
|
|
"multiplayer_modes": [],
|
|
"game_localizations": [{"name": n} for n in (game_localizations or [])],
|
|
}
|
|
|
|
|
|
class TestSearchRomGameTypeFilter:
|
|
"""Tests for _search_rom game_type filtering."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_standalone_expansion_included_in_game_type_filter(self):
|
|
"""Searching with game_type filter must include STANDALONE_EXPANSION
|
|
so that games like 'Ecco: The Tides of Time' are found on the first
|
|
search pass and not confused with their parent game."""
|
|
handler = IGDBHandler()
|
|
|
|
ecco_dolphin = _make_game(1799, "Ecco the Dolphin")
|
|
ecco_tides = _make_game(5379, "Ecco: The Tides of Time")
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
# First call (with game_type filter): return both games
|
|
if where and "game_type" in where:
|
|
# Verify STANDALONE_EXPANSION (4) is in the filter
|
|
assert (
|
|
str(int(GameType.STANDALONE_EXPANSION)) in where
|
|
), f"STANDALONE_EXPANSION should be in game_type filter, got: {where}"
|
|
# Simulate IGDB returning both games when the search includes
|
|
# standalone expansions
|
|
if search_term and "tides of time" in search_term.lower():
|
|
return [ecco_dolphin, ecco_tides]
|
|
return [ecco_dolphin]
|
|
return []
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"ecco the tides of time", GENESIS_IGDB_ID, with_game_type=True
|
|
)
|
|
|
|
assert result is not None
|
|
assert (
|
|
result["id"] == 5379
|
|
), f"Expected Ecco: The Tides of Time (id=5379), got {result.get('name')} (id={result.get('id')})"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_expanded_search_uses_all_results_not_just_first(self):
|
|
"""When the primary search fails and the expanded IGDB search endpoint
|
|
is used, all unique game IDs from the results must be fetched and
|
|
the best match selected — not just the first result."""
|
|
handler = IGDBHandler()
|
|
|
|
ecco_dolphin = _make_game(1799, "Ecco the Dolphin")
|
|
ecco_tides = _make_game(5379, "Ecco: The Tides of Time")
|
|
|
|
# Primary search returns nothing useful
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
if where and "game_type" not in where and not where.startswith("("):
|
|
# Primary search pass — return no results so we fall through to
|
|
# the expanded search
|
|
return []
|
|
if where and where.startswith("("):
|
|
# Expanded game details lookup — return both candidates
|
|
return [ecco_dolphin, ecco_tides]
|
|
return []
|
|
|
|
# Expanded search returns two results — wrong game FIRST, correct game second
|
|
expanded_results = [
|
|
{"game": {"id": 1799}, "name": "Ecco the Dolphin"},
|
|
{"game": {"id": 5379}, "name": "Ecco: The Tides of Time"},
|
|
]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=expanded_results,
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"ecco the tides of time", GENESIS_IGDB_ID, with_game_type=False
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 5379, (
|
|
f"Expected Ecco: The Tides of Time (id=5379), got {result.get('name')} (id={result.get('id')}). "
|
|
"The expanded search must consider ALL results, not just the first."
|
|
)
|
|
|
|
|
|
class TestSearchRomLocalizedNames:
|
|
"""Tests for matching ROMs by localized / alternative titles.
|
|
|
|
Regression coverage for issue #3435: a ROM whose No-Intro / ReDump filename
|
|
uses a localized title (e.g. ``007 - Die Welt Ist Nicht Genug (Germany)``)
|
|
must match the IGDB game that lists that title in ``alternative_names`` or
|
|
``game_localizations``, not only the primary English ``name``.
|
|
"""
|
|
|
|
# James Bond 007: The World Is Not Enough, IGDB id 158962, has the German
|
|
# alternative name "007 - Die Welt Ist Nicht Genug" (issue #3435).
|
|
ENGLISH_NAME = "James Bond 007: The World Is Not Enough"
|
|
GERMAN_TITLE = "007 - Die Welt Ist Nicht Genug"
|
|
GAME_ID = 158962
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alt_name_match_in_primary_games_search(self):
|
|
"""A localized filename must match when IGDB returns the game on the
|
|
primary games-endpoint pass and the term only matches an alternative
|
|
name, not the primary English name."""
|
|
handler = IGDBHandler()
|
|
|
|
game = _make_game(
|
|
self.GAME_ID,
|
|
self.ENGLISH_NAME,
|
|
alternative_names=[self.GERMAN_TITLE],
|
|
)
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
# Primary games-endpoint pass returns the game (IGDB's fuzzy search
|
|
# surfaces it via the alt name), but the term won't match the
|
|
# English primary name on its own.
|
|
return [game]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"007 die welt ist nicht genug", GENESIS_IGDB_ID
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == self.GAME_ID, (
|
|
f"Expected {self.ENGLISH_NAME} (id={self.GAME_ID}) via its German "
|
|
f"alternative name, got {result.get('name')} (id={result.get('id')})"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alt_name_match_in_expanded_search(self):
|
|
"""A localized filename must match when the game is only discovered via
|
|
the expanded ``/search`` alternative_name query and the term matches an
|
|
alternative name rather than the primary English name."""
|
|
handler = IGDBHandler()
|
|
|
|
game = _make_game(
|
|
self.GAME_ID,
|
|
self.ENGLISH_NAME,
|
|
alternative_names=[self.GERMAN_TITLE],
|
|
)
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
# Expanded game-details lookup (id filter) returns the full game.
|
|
if where and where.startswith("("):
|
|
return [game]
|
|
# Primary pass returns nothing useful.
|
|
return []
|
|
|
|
expanded_results = [{"game": {"id": self.GAME_ID}, "name": self.ENGLISH_NAME}]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=expanded_results,
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"007 die welt ist nicht genug", GENESIS_IGDB_ID
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == self.GAME_ID, (
|
|
f"Expected {self.ENGLISH_NAME} (id={self.GAME_ID}) via its German "
|
|
f"alternative name, got {result.get('name')} (id={result.get('id')})"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_localization_name_match_in_expanded_search(self):
|
|
"""A localized filename must match when the matching title lives in
|
|
``game_localizations`` rather than ``alternative_names``."""
|
|
handler = IGDBHandler()
|
|
|
|
game = _make_game(
|
|
self.GAME_ID,
|
|
self.ENGLISH_NAME,
|
|
game_localizations=[self.GERMAN_TITLE],
|
|
)
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
if where and where.startswith("("):
|
|
return [game]
|
|
return []
|
|
|
|
expanded_results = [{"game": {"id": self.GAME_ID}, "name": self.ENGLISH_NAME}]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=expanded_results,
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"007 die welt ist nicht genug", GENESIS_IGDB_ID
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == self.GAME_ID
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_primary_english_name_still_matches(self):
|
|
"""Indexing alternative titles must not regress matching by the primary
|
|
English name."""
|
|
handler = IGDBHandler()
|
|
|
|
game = _make_game(
|
|
self.GAME_ID,
|
|
self.ENGLISH_NAME,
|
|
alternative_names=[self.GERMAN_TITLE],
|
|
)
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
return [game]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"james bond 007 the world is not enough", GENESIS_IGDB_ID
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == self.GAME_ID
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_primary_name_wins_over_other_games_alt_name(self):
|
|
"""When a search term equals one game's primary name and another game's
|
|
alternative name, the primary-name owner must win (alt titles fill in
|
|
only names not already claimed by a primary name)."""
|
|
handler = IGDBHandler()
|
|
|
|
primary = _make_game(100, "Contra")
|
|
# A different, higher-id game that lists "Contra" as an alt title.
|
|
other = _make_game(200, "Probotector", alternative_names=["Contra"])
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
return [other, primary]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom("contra", GENESIS_IGDB_ID)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 100, (
|
|
"Expected the game whose primary name is 'Contra' (id=100), not the "
|
|
f"game that merely lists it as an alternative name; got id={result.get('id')}"
|
|
)
|
|
|
|
|
|
class TestRegionalTwinPlatformHelpers:
|
|
"""Unit tests for the regional-twin platform helpers (issue #3462).
|
|
|
|
IGDB files a console and its regional twin (SNES/Super Famicom,
|
|
NES/Famicom) under separate platform ids, so a search must include both to
|
|
match region-exclusive titles.
|
|
"""
|
|
|
|
def test_twin_pairs_are_bidirectional(self):
|
|
"""Each console resolves to its regional twin in both directions."""
|
|
assert _platform_igdb_ids_with_twin(SNES_IGDB_ID) == [
|
|
SNES_IGDB_ID,
|
|
SUPER_FAMICOM_IGDB_ID,
|
|
]
|
|
assert _platform_igdb_ids_with_twin(SUPER_FAMICOM_IGDB_ID) == [
|
|
SUPER_FAMICOM_IGDB_ID,
|
|
SNES_IGDB_ID,
|
|
]
|
|
assert _platform_igdb_ids_with_twin(NES_IGDB_ID) == [
|
|
NES_IGDB_ID,
|
|
FAMICOM_IGDB_ID,
|
|
]
|
|
assert _platform_igdb_ids_with_twin(FAMICOM_IGDB_ID) == [
|
|
FAMICOM_IGDB_ID,
|
|
NES_IGDB_ID,
|
|
]
|
|
|
|
def test_non_twin_platform_has_no_twin(self):
|
|
"""A platform without a regional twin resolves to itself only."""
|
|
assert _platform_igdb_ids_with_twin(GENESIS_IGDB_ID) == [GENESIS_IGDB_ID]
|
|
|
|
def test_build_where_single_platform_is_unparenthesized(self):
|
|
"""A non-twin platform keeps the original single-clause shape."""
|
|
assert (
|
|
_build_platforms_where(GENESIS_IGDB_ID) == f"platforms=[{GENESIS_IGDB_ID}]"
|
|
)
|
|
assert (
|
|
_build_platforms_where(GENESIS_IGDB_ID, field="game.platforms")
|
|
== f"game.platforms=[{GENESIS_IGDB_ID}]"
|
|
)
|
|
|
|
def test_build_where_twin_platform_is_an_or_group(self):
|
|
"""A twin platform produces a parenthesized OR of both platform ids."""
|
|
assert (
|
|
_build_platforms_where(SNES_IGDB_ID)
|
|
== f"(platforms=[{SNES_IGDB_ID}] | platforms=[{SUPER_FAMICOM_IGDB_ID}])"
|
|
)
|
|
assert (
|
|
_build_platforms_where(NES_IGDB_ID, field="game.platforms")
|
|
== f"(game.platforms=[{NES_IGDB_ID}] | game.platforms=[{FAMICOM_IGDB_ID}])"
|
|
)
|
|
|
|
|
|
class TestSearchRomRegionalTwinPlatforms:
|
|
"""Tests that IGDB search includes a platform's regional twin (issue #3462).
|
|
|
|
A Japan-only Super Famicom title (e.g. *Rudra no Hihou*) lives only under
|
|
IGDB's Super Famicom platform, so an ``snes`` scan that filtered to the SNES
|
|
platform alone would silently drop it. The search must query both twins.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_snes_search_matches_super_famicom_only_game(self):
|
|
"""A Super-Famicom-only game must match when scanned from ``snes``."""
|
|
handler = IGDBHandler()
|
|
|
|
# Rudra no Hihou is catalogued under Super Famicom (58) only.
|
|
rudra = _make_game(829, "Rudra no Hihou")
|
|
captured_wheres: list[str] = []
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
captured_wheres.append(where or "")
|
|
# IGDB only surfaces the game when the Super Famicom platform is
|
|
# part of the filter.
|
|
if where and f"platforms=[{SUPER_FAMICOM_IGDB_ID}]" in where:
|
|
return [rudra]
|
|
return []
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"rudra no hihou", SNES_IGDB_ID, with_game_type=True
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 829, (
|
|
"Expected Rudra no Hihou (id=829) to match from an snes scan via the "
|
|
f"Super Famicom platform; got {result.get('name')} (id={result.get('id')})"
|
|
)
|
|
# The primary platform filter must mention both twins.
|
|
assert any(
|
|
f"platforms=[{SNES_IGDB_ID}]" in w
|
|
and f"platforms=[{SUPER_FAMICOM_IGDB_ID}]" in w
|
|
for w in captured_wheres
|
|
), f"Expected SNES + Super Famicom in the platform filter, got: {captured_wheres}"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nes_search_matches_famicom_only_game(self):
|
|
"""A Famicom-only game must match when scanned from ``nes``."""
|
|
handler = IGDBHandler()
|
|
|
|
famicom_only = _make_game(1234, "Famicom Mukashi Banashi")
|
|
captured_wheres: list[str] = []
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
captured_wheres.append(where or "")
|
|
if where and f"platforms=[{FAMICOM_IGDB_ID}]" in where:
|
|
return [famicom_only]
|
|
return []
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"famicom mukashi banashi", NES_IGDB_ID, with_game_type=True
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 1234
|
|
assert any(
|
|
f"platforms=[{NES_IGDB_ID}]" in w and f"platforms=[{FAMICOM_IGDB_ID}]" in w
|
|
for w in captured_wheres
|
|
), f"Expected NES + Famicom in the platform filter, got: {captured_wheres}"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_super_famicom_search_matches_snes_only_game(self):
|
|
"""A Western-only SNES game must match when scanned from ``sfam``."""
|
|
handler = IGDBHandler()
|
|
|
|
snes_only = _make_game(4321, "EarthBound")
|
|
captured_wheres: list[str] = []
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
captured_wheres.append(where or "")
|
|
if where and f"platforms=[{SNES_IGDB_ID}]" in where:
|
|
return [snes_only]
|
|
return []
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"earthbound", SUPER_FAMICOM_IGDB_ID, with_game_type=True
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 4321
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_twin_platform_filter_is_single_platform(self):
|
|
"""A platform without a twin must keep querying only its own id."""
|
|
handler = IGDBHandler()
|
|
|
|
game = _make_game(1799, "Ecco the Dolphin")
|
|
captured_wheres: list[str] = []
|
|
|
|
async def mock_list_games(
|
|
search_term=None, fields=None, where=None, limit=None
|
|
):
|
|
captured_wheres.append(where or "")
|
|
return [game]
|
|
|
|
with (
|
|
patch(
|
|
"handler.metadata.igdb_handler.IGDBHandler.is_enabled",
|
|
return_value=True,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"list_games",
|
|
side_effect=mock_list_games,
|
|
),
|
|
patch.object(
|
|
handler.igdb_service,
|
|
"search",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
):
|
|
result = await handler._search_rom(
|
|
"ecco the dolphin", GENESIS_IGDB_ID, with_game_type=True
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["id"] == 1799
|
|
# No twin → no OR group, just the single platform clause.
|
|
assert all(
|
|
" | " not in w for w in captured_wheres
|
|
), f"Non-twin platform should not OR a twin platform, got: {captured_wheres}"
|
|
assert any(
|
|
f"platforms=[{GENESIS_IGDB_ID}]" in w for w in captured_wheres
|
|
), captured_wheres
|