/
/
/
1"""Unit tests for the config value validator."""
2
3# ruff: noqa: D103 -- test functions don't need docstrings
4
5from __future__ import annotations
6
7from typing import Any
8
9import pytest
10from fastmcp.exceptions import ToolError
11from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption
12from music_assistant_models.enums import ConfigEntryType
13
14from music_assistant.providers.fastmcp_server.config_io.validator import coerce
15
16
17def _entry(**kw: Any) -> ConfigEntry:
18 base = {"key": "k", "type": ConfigEntryType.STRING, "label": "K"}
19 base.update(kw)
20 return ConfigEntry(**base) # type: ignore[arg-type]
21
22
23def test_coerce_type_coercion_str_to_int() -> None:
24 entry = _entry(type=ConfigEntryType.INTEGER)
25 assert coerce(entry, "8099") == 8099
26
27
28def test_coerce_type_coercion_str_to_bool() -> None:
29 entry = _entry(type=ConfigEntryType.BOOLEAN)
30 assert coerce(entry, "1") is True
31
32
33def test_coerce_type_coercion_int_to_float() -> None:
34 entry = _entry(type=ConfigEntryType.FLOAT)
35 assert coerce(entry, 42) == 42.0
36
37
38def test_coerce_passes_through_validation_callback() -> None:
39 def check_positive(val: int) -> bool:
40 return val > 0
41
42 entry = _entry(type=ConfigEntryType.INTEGER, validate=check_positive)
43 with pytest.raises(ToolError, match="failed validation"):
44 coerce(entry, -5)
45
46
47def test_coerce_int_out_of_range_rejected() -> None:
48 entry = _entry(type=ConfigEntryType.INTEGER, range=(1, 15))
49 with pytest.raises(ToolError, match="failed validation"):
50 coerce(entry, 999)
51
52
53def test_coerce_string_not_in_options_rejected() -> None:
54 entry = _entry(
55 type=ConfigEntryType.STRING,
56 options=[ConfigValueOption(title="A", value="a"), ConfigValueOption(title="B", value="b")],
57 )
58 with pytest.raises(ToolError, match="failed validation"):
59 coerce(entry, "z")
60
61
62def test_coerce_int_in_range_ok() -> None:
63 entry = _entry(type=ConfigEntryType.INTEGER, range=(1, 15))
64 assert coerce(entry, 8) == 8
65
66
67def test_coerce_string_in_options_ok() -> None:
68 entry = _entry(
69 type=ConfigEntryType.STRING,
70 options=[ConfigValueOption(title="A", value="a"), ConfigValueOption(title="B", value="b")],
71 )
72 assert coerce(entry, "a") == "a"
73
74
75def test_coerce_boolean_with_stray_range_not_range_checked() -> None:
76 # bool subclasses int â a BOOLEAN entry that carries a range must NOT
77 # be range-checked (False == 0 would spuriously fail range=(1,15)).
78 entry = _entry(type=ConfigEntryType.BOOLEAN, range=(1, 15))
79 assert coerce(entry, False) is False
80 assert coerce(entry, True) is True
81
82
83def test_coerce_multi_value_options_checks_each_member() -> None:
84 entry = _entry(
85 type=ConfigEntryType.STRING,
86 multi_value=True,
87 options=[ConfigValueOption(title="A", value="a"), ConfigValueOption(title="B", value="b")],
88 )
89 # all members valid â passes, returns the list
90 assert coerce(entry, ["a", "b"]) == ["a", "b"]
91 # a bad member â ToolError, NOT a raw TypeError
92 with pytest.raises(ToolError, match="failed validation"):
93 coerce(entry, ["a", "z"])
94