From 73710acb4ce8ae37cb177f6edfbb0d1a7eea149f Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:24:34 +0200 Subject: [PATCH] Preserve middleware data across scene transitions (#1687) (#1766) * Preserve middleware context across scene goto transitions (#1687) * Add After.goto coverage for scene middleware context (#1687) --- CHANGES/1687.bugfix.rst | 1 + aiogram/fsm/scene.py | 4 + tests/test_fsm/test_scene.py | 2 + ...t_1687_scene_goto_loses_middleware_data.py | 106 ++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 CHANGES/1687.bugfix.rst create mode 100644 tests/test_issues/test_1687_scene_goto_loses_middleware_data.py diff --git a/CHANGES/1687.bugfix.rst b/CHANGES/1687.bugfix.rst new file mode 100644 index 00000000..9fac31c9 --- /dev/null +++ b/CHANGES/1687.bugfix.rst @@ -0,0 +1 @@ +Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``. diff --git a/aiogram/fsm/scene.py b/aiogram/fsm/scene.py index da0a52d2..4c7fa72c 100644 --- a/aiogram/fsm/scene.py +++ b/aiogram/fsm/scene.py @@ -259,6 +259,7 @@ class SceneHandlerWrapper: ) raise SceneException(msg) from None event_update: Update = kwargs["event_update"] + scenes.data = {**scenes.data, **kwargs} scene = self.scene( wizard=SceneWizard( scene_config=self.scene.__scene_config__, @@ -712,6 +713,9 @@ class ScenesManager: :param kwargs: Additional keyword arguments to pass to the scene's wizard.enter() method. :return: None """ + if kwargs: + self.data = {**self.data, **kwargs} + if _check_active: active_scene = await self._get_active_scene() if active_scene is not None: diff --git a/tests/test_fsm/test_scene.py b/tests/test_fsm/test_scene.py index 3a9944b0..ba601325 100644 --- a/tests/test_fsm/test_scene.py +++ b/tests/test_fsm/test_scene.py @@ -253,6 +253,7 @@ class TestSceneHandlerWrapper: state_mock = AsyncMock(spec=FSMContext) scenes_mock = AsyncMock(spec=ScenesManager) + scenes_mock.data = {} event_update_mock = Update( update_id=42, message=Message( @@ -282,6 +283,7 @@ class TestSceneHandlerWrapper: state_mock = AsyncMock(spec=FSMContext) scenes_mock = AsyncMock(spec=ScenesManager) + scenes_mock.data = {} event_update_mock = Update( update_id=42, message=Message( diff --git a/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py new file mode 100644 index 00000000..d0433298 --- /dev/null +++ b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py @@ -0,0 +1,106 @@ +from collections.abc import Awaitable, Callable +from datetime import datetime +from typing import Any + +from aiogram import BaseMiddleware, Dispatcher +from aiogram.enums import ChatType +from aiogram.filters import CommandStart +from aiogram.fsm.scene import After, Scene, SceneRegistry, on +from aiogram.types import Chat, Message, TelegramObject, Update, User +from tests.mocked_bot import MockedBot + + +class TestContextMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], + ) -> Any: + data["test_context"] = "context from middleware" + return await handler(event, data) + + +class TargetScene(Scene, state="target"): + entered_with_context: str | None = None + + @on.message.enter() + async def on_enter(self, message: Message, test_context: str) -> None: + type(self).entered_with_context = test_context + + +class StartScene(Scene, state="start"): + @on.message.enter() + async def on_start(self, message: Message) -> None: + await self.wizard.goto(TargetScene) + + +class StartSceneWithAfter(Scene, state="start_with_after"): + @on.message(after=After.goto(TargetScene)) + async def goto_target_with_after(self, message: Message) -> None: + pass + + +async def test_scene_goto_preserves_message_middleware_data(bot: MockedBot) -> None: + dp = Dispatcher() + registry = SceneRegistry(dp) + registry.add(StartScene, TargetScene) + dp.message.register(StartScene.as_handler(), CommandStart()) + dp.message.middleware(TestContextMiddleware()) + + TargetScene.entered_with_context = None + + update = Update( + update_id=1, + message=Message( + message_id=1, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="/start", + ), + ) + + await dp.feed_update(bot, update) + + assert TargetScene.entered_with_context == "context from middleware" + + +async def test_scene_after_goto_preserves_message_middleware_data(bot: MockedBot) -> None: + dp = Dispatcher() + registry = SceneRegistry(dp) + registry.add(StartSceneWithAfter, TargetScene) + dp.message.register(StartSceneWithAfter.as_handler(), CommandStart()) + dp.message.middleware(TestContextMiddleware()) + + TargetScene.entered_with_context = None + + await dp.feed_update( + bot, + Update( + update_id=1, + message=Message( + message_id=1, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="/start", + ), + ), + ) + + await dp.feed_update( + bot, + Update( + update_id=2, + message=Message( + message_id=2, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="go", + ), + ), + ) + + assert TargetScene.entered_with_context == "context from middleware"