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