/
/
/
1"""Tests for the MCP Connect Wizard setup flow."""
2
3from __future__ import annotations
4
5import asyncio
6import time
7from typing import Any
8from unittest import mock
9
10from aiohttp.test_utils import make_mocked_request
11from music_assistant_models.enums import FlowStepType
12
13from music_assistant.models.setup_flow import SetupFlowContext, SetupSession
14from music_assistant.providers.fastmcp_server import setup_flow
15
16
17async def _wait_for(predicate: Any) -> Any:
18 """Wait for a setup-flow state transition."""
19 deadline = time.monotonic() + 5
20 while time.monotonic() < deadline:
21 if result := predicate():
22 return result
23 await asyncio.sleep(0.01)
24 raise AssertionError("setup-flow state did not change")
25
26
27async def test_setup_mounts_wizard_and_finishes_after_callback() -> None:
28 """A new provider stays in setup until the Connect Wizard signals completion."""
29 routes: dict[str, Any] = {}
30 mass = mock.Mock()
31 mass.webserver.base_url = "http://localhost:8095/ma"
32 mass.players.all_players.return_value = []
33 mass.get_provider.return_value = None
34
35 def register_dynamic_route(path: str, handler: Any, method: str = "*") -> Any:
36 del method
37 routes[path] = handler
38 return lambda: routes.pop(path, None)
39
40 mass.webserver.register_dynamic_route.side_effect = register_dynamic_route
41 collected: dict[str, Any] = {}
42
43 async def finish(_session: SetupSession, values: dict[str, Any]) -> dict[str, str]:
44 collected.update(values)
45 return {"instance_id": "fastmcp_server"}
46
47 session = SetupSession(
48 mass,
49 "a1b2",
50 SetupFlowContext(kind="setup", reason="user", domain="fastmcp_server"),
51 finish,
52 )
53 unmount = mock.Mock()
54 with (
55 mock.patch.object(
56 setup_flow,
57 "mount_connect_wizard",
58 mock.AsyncMock(return_value=unmount),
59 ) as mount,
60 mock.patch.object(
61 setup_flow,
62 "_dispatch_open_connect",
63 mock.AsyncMock(return_value="http://localhost:8095/ma/mcp/v1/connect"),
64 ) as dispatch,
65 ):
66 task = asyncio.create_task(setup_flow.run_setup(session))
67 step = await _wait_for(
68 lambda: (
69 session.current_step
70 if session.current_step and session.current_step.type == FlowStepType.EXTERNAL
71 else None
72 )
73 )
74 assert not session.finished
75 callback_path = f"/setup_flow/callback/{session.flow_id}"
76 await routes[callback_path](make_mocked_request("GET", callback_path))
77 await _wait_for(lambda: session.finished)
78 await task
79
80 mount.assert_awaited_once()
81 dispatch.assert_awaited_once()
82 assert dispatch.await_args is not None
83 assert dispatch.await_args.kwargs["setup_callback_path"] == callback_path
84 unmount.assert_called_once_with()
85 assert collected == {}
86 assert step.step_id == "connect"
87