/
/
1"""Tests for the MPD player setup flow."""
2
3from __future__ import annotations
4
5from collections.abc import AsyncGenerator
6from typing import cast
7from unittest.mock import AsyncMock, MagicMock, patch
8
9import pytest
10from music_assistant_models.config_entries import PlayerConfig
11from music_assistant_models.enums import FlowStepType
12
13from music_assistant.constants import CONF_PASSWORD, CONF_PLAYERS
14from music_assistant.mass import MusicAssistant
15from music_assistant.providers.mpd.player import MPDPlayer
16from tests.common import MockProvider, create_mock_config
17
18
19@pytest.fixture
20async def flow_mass(mass_minimal: MusicAssistant) -> AsyncGenerator[MusicAssistant]:
21 """
22 Provide a minimal server suitable for driving player setup flows through the engine.
23
24 Builds on mass_minimal (config controller only) and stubs the players controller
25 surface the flow engine touches, mirroring the flow_mass fixture in
26 tests/controllers/config/test_setup_flows.py.
27 """
28 mass_minimal.players = MagicMock()
29 mass_minimal.players.on_player_config_change = AsyncMock()
30 try:
31 yield mass_minimal
32 finally:
33 for flow in list(mass_minimal.config._setup_flows.values()):
34 await mass_minimal.config._abort_flow(flow, reason="aborted")
35 if (sweep_handle := mass_minimal.config._flow_sweep_handle) is not None:
36 sweep_handle.cancel()
37
38
39def _make_player(player_id: str = "mpd_test") -> MPDPlayer:
40 """Build a real MPDPlayer with a decoupled (mocked) provider/mass."""
41 provider = MockProvider("mpd", instance_id="mpd--1")
42 provider.mass.config.get_base_player_config.return_value = create_mock_config("MPD Test")
43 return MPDPlayer(provider=provider, player_id=player_id, host="10.0.0.6") # type: ignore[arg-type]
44
45
46async def test_get_config_entries_excludes_password() -> None:
47 """The password is flow-managed now and must not appear in the options config entries."""
48 player = _make_player()
49 entries = await player.get_config_entries()
50 assert CONF_PASSWORD not in {entry.key for entry in entries}
51
52
53async def test_on_config_updated_reads_password_via_setup_value() -> None:
54 """on_config_updated resolves the password from setup_data, not the options config."""
55 player = _make_player()
56 cast("MagicMock", player.mass.config.get).return_value = {CONF_PASSWORD: "encrypted-blob"}
57 cast("MagicMock", player.mass.config.decrypt_string).return_value = "secret"
58 with (
59 patch.object(player, "_connect", AsyncMock()),
60 patch.object(player, "_disconnect", AsyncMock()),
61 ):
62 await player.on_config_updated()
63 assert player.password == "secret"
64 assert player.needs_setup is False
65 assert player.setup_reason is None
66
67
68async def test_setup_flow_collects_and_persists_password(flow_mass: MusicAssistant) -> None:
69 """The setup flow shows a single password field and persists it encrypted."""
70 player_id = "mpd_test"
71 player = _make_player(player_id)
72 flow_mass.config.set(
73 f"{CONF_PLAYERS}/{player_id}",
74 {"player_id": player_id, "provider": player.provider_id, "enabled": True},
75 )
76 player_config = PlayerConfig(values={}, provider=player.provider_id, player_id=player_id)
77 with (
78 patch.object(flow_mass.players, "get_player", return_value=player),
79 patch.object(flow_mass.config, "get_player_config", AsyncMock(return_value=player_config)),
80 ):
81 step = await flow_mass.config.setup_player(player_id)
82 assert step.type == FlowStepType.FORM
83 assert {entry.key for entry in step.entries} == {CONF_PASSWORD}
84 finish_step = await flow_mass.config.submit_setup_flow(
85 step.flow_id, {CONF_PASSWORD: "secret"}
86 )
87 assert finish_step.type == FlowStepType.FINISH
88 assert finish_step.result == {"player_id": player_id}
89 raw_conf = flow_mass.config.get(f"{CONF_PLAYERS}/{player_id}")
90 assert flow_mass.config.decrypt_string(raw_conf["setup_data"][CONF_PASSWORD]) == "secret"
91