/
/
/
1"""Tests for provider manifest loading, in particular the has_setup_flow flag."""
2
3from __future__ import annotations
4
5import json
6import pathlib
7from typing import TYPE_CHECKING
8
9import pytest
10
11from music_assistant import mass as mass_module
12
13if TYPE_CHECKING:
14 from music_assistant.mass import MusicAssistant
15
16
17async def _load_manifests(mass_minimal: MusicAssistant) -> None:
18 """Run the (name-mangled, private) manifest loader against PROVIDERS_PATH."""
19 mass_minimal._provider_manifests.clear()
20 await mass_minimal._MusicAssistant__load_provider_manifests() # type: ignore[attr-defined]
21
22
23def _write_provider(
24 providers_dir: pathlib.Path, domain: str, *, setup_flow_source: str | None
25) -> None:
26 """Write a minimal provider package with an optional setup_flow.py module."""
27 provider_dir = providers_dir / domain
28 provider_dir.mkdir()
29 (provider_dir / "__init__.py").write_text("")
30 (provider_dir / "manifest.json").write_text(
31 json.dumps(
32 {
33 "type": "music",
34 "domain": domain,
35 "name": domain,
36 "description": "test provider",
37 "codeowners": [],
38 }
39 )
40 )
41 if setup_flow_source is not None:
42 (provider_dir / "setup_flow.py").write_text(setup_flow_source)
43
44
45@pytest.fixture
46def fake_providers_dir(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
47 """Redirect PROVIDERS_PATH to a throwaway directory for the duration of the test."""
48 providers_dir = tmp_path / "providers"
49 providers_dir.mkdir()
50 monkeypatch.setattr(mass_module, "PROVIDERS_PATH", str(providers_dir))
51 return providers_dir
52
53
54async def test_has_setup_flow_true_when_module_present(
55 mass_minimal: MusicAssistant, fake_providers_dir: pathlib.Path
56) -> None:
57 """A provider shipping a setup_flow.py module is flagged as having one, including over the API."""
58 _write_provider(fake_providers_dir, "with_flow", setup_flow_source="")
59 await _load_manifests(mass_minimal)
60 manifest = mass_minimal.get_provider_manifest("with_flow")
61 assert manifest.has_setup_flow is True
62 assert manifest.to_dict()["has_setup_flow"] is True
63 assert manifest in mass_minimal.get_provider_manifests()
64
65
66async def test_has_setup_flow_false_when_module_absent(
67 mass_minimal: MusicAssistant, fake_providers_dir: pathlib.Path
68) -> None:
69 """A provider without a setup_flow.py module is flagged as not having one, including over the API."""
70 _write_provider(fake_providers_dir, "without_flow", setup_flow_source=None)
71 await _load_manifests(mass_minimal)
72 manifest = mass_minimal.get_provider_manifest("without_flow")
73 assert manifest.has_setup_flow is False
74 assert manifest.to_dict()["has_setup_flow"] is False
75
76
77async def test_has_setup_flow_ignores_internal_import_errors(
78 mass_minimal: MusicAssistant, fake_providers_dir: pathlib.Path
79) -> None:
80 """
81 A setup_flow.py that itself fails to import still counts as present.
82
83 The flag is derived from the file's mere existence, not from actually
84 importing it, so a bug inside the module must not produce a false negative
85 here; that failure only ever surfaces later, when the flow is started.
86 """
87 _write_provider(
88 fake_providers_dir, "broken_flow", setup_flow_source="raise ImportError('boom')"
89 )
90 await _load_manifests(mass_minimal)
91 assert mass_minimal.get_provider_manifest("broken_flow").has_setup_flow is True
92