| """Shared fixtures for gateway tests. |
| |
| The ``_ensure_telegram_mock`` helper guarantees that a minimal mock of |
| the ``telegram`` package is registered in :data:`sys.modules` **before** |
| any test file triggers ``from gateway.platforms.telegram import ...``. |
| |
| Without this, ``pytest-xdist`` workers that happen to collect |
| ``test_telegram_caption_merge.py`` (bare top-level import, no per-file |
| mock) first will cache ``ChatType = None`` from the production |
| ImportError fallback, causing 30+ downstream test failures wherever |
| ``ChatType.GROUP`` / ``ChatType.SUPERGROUP`` is accessed. |
| |
| Individual test files may still call their own ``_ensure_telegram_mock`` |
| — it short-circuits when the mock is already present. |
| """ |
|
|
| import sys |
| from unittest.mock import MagicMock |
|
|
|
|
| def _ensure_telegram_mock() -> None: |
| """Install a comprehensive telegram mock in sys.modules. |
| |
| Idempotent — skips when the real library is already imported. |
| Uses ``sys.modules[name] = mod`` (overwrite) instead of |
| ``setdefault`` so it wins even if a partial/broken import |
| already cached a module with ``ChatType = None``. |
| """ |
| if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"): |
| return |
|
|
| mod = MagicMock() |
| mod.ext.ContextTypes.DEFAULT_TYPE = type(None) |
| mod.constants.ParseMode.MARKDOWN = "Markdown" |
| mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2" |
| mod.constants.ParseMode.HTML = "HTML" |
| mod.constants.ChatType.PRIVATE = "private" |
| mod.constants.ChatType.GROUP = "group" |
| mod.constants.ChatType.SUPERGROUP = "supergroup" |
| mod.constants.ChatType.CHANNEL = "channel" |
|
|
| |
| |
| mod.error.NetworkError = type("NetworkError", (OSError,), {}) |
| mod.error.TimedOut = type("TimedOut", (OSError,), {}) |
| mod.error.BadRequest = type("BadRequest", (Exception,), {}) |
| mod.error.Forbidden = type("Forbidden", (Exception,), {}) |
| mod.error.InvalidToken = type("InvalidToken", (Exception,), {}) |
| mod.error.RetryAfter = type("RetryAfter", (Exception,), {"retry_after": 1}) |
| mod.error.Conflict = type("Conflict", (Exception,), {}) |
|
|
| |
| mod.Update.ALL_TYPES = [] |
|
|
| for name in ( |
| "telegram", |
| "telegram.ext", |
| "telegram.constants", |
| "telegram.request", |
| ): |
| sys.modules[name] = mod |
| sys.modules["telegram.error"] = mod.error |
|
|
|
|
| def _ensure_discord_mock() -> None: |
| """Install a comprehensive discord mock in sys.modules. |
| |
| Idempotent — skips when the real library is already imported. |
| Uses ``sys.modules[name] = mod`` (overwrite) instead of |
| ``setdefault`` so it wins even if a partial/broken import already |
| cached the module. |
| |
| This mock is comprehensive — it includes **all** attributes needed by |
| every gateway discord test file. Individual test files should call |
| this function (it short-circuits when already present) rather than |
| maintaining their own mock setup. |
| """ |
| if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"): |
| return |
|
|
| from types import SimpleNamespace |
|
|
| discord_mod = MagicMock() |
| discord_mod.Intents.default.return_value = MagicMock() |
| discord_mod.Client = MagicMock |
| discord_mod.File = MagicMock |
| discord_mod.DMChannel = type("DMChannel", (), {}) |
| discord_mod.Thread = type("Thread", (), {}) |
| discord_mod.ForumChannel = type("ForumChannel", (), {}) |
| discord_mod.Interaction = object |
| discord_mod.Embed = MagicMock |
| discord_mod.ui = SimpleNamespace( |
| View=object, |
| button=lambda *a, **k: (lambda fn: fn), |
| Button=object, |
| ) |
| discord_mod.ButtonStyle = SimpleNamespace( |
| success=1, primary=2, secondary=2, danger=3, |
| green=1, grey=2, blurple=2, red=3, |
| ) |
| discord_mod.Color = SimpleNamespace( |
| orange=lambda: 1, green=lambda: 2, blue=lambda: 3, |
| red=lambda: 4, purple=lambda: 5, |
| ) |
|
|
| |
| class _FakeGroup: |
| def __init__(self, *, name, description, parent=None): |
| self.name = name |
| self.description = description |
| self.parent = parent |
| self._children: dict = {} |
| if parent is not None: |
| parent.add_command(self) |
|
|
| def add_command(self, cmd): |
| self._children[cmd.name] = cmd |
|
|
| class _FakeCommand: |
| def __init__(self, *, name, description, callback, parent=None): |
| self.name = name |
| self.description = description |
| self.callback = callback |
| self.parent = parent |
|
|
| discord_mod.app_commands = SimpleNamespace( |
| describe=lambda **kwargs: (lambda fn: fn), |
| choices=lambda **kwargs: (lambda fn: fn), |
| Choice=lambda **kwargs: SimpleNamespace(**kwargs), |
| Group=_FakeGroup, |
| Command=_FakeCommand, |
| ) |
|
|
| ext_mod = MagicMock() |
| commands_mod = MagicMock() |
| commands_mod.Bot = MagicMock |
| ext_mod.commands = commands_mod |
|
|
| for name in ("discord", "discord.ext", "discord.ext.commands"): |
| sys.modules[name] = discord_mod |
| sys.modules["discord.ext"] = ext_mod |
| sys.modules["discord.ext.commands"] = commands_mod |
|
|
|
|
| |
| _ensure_telegram_mock() |
| _ensure_discord_mock() |
|
|