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)
This commit is contained in:
Kostiantyn Kriuchkov 2026-02-15 20:24:34 +02:00 committed by GitHub
parent e37eddbe8c
commit 73710acb4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 0 deletions

1
CHANGES/1687.bugfix.rst Normal file
View file

@ -0,0 +1 @@
Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``.

View file

@ -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:

View file

@ -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(

View file

@ -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"