Web frontend
This commit is contained in:
176
python-backend/tests/test_settings.py
Normal file
176
python-backend/tests/test_settings.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Parent mode: the small set of settings the UI may change, and how they are saved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from pathlib import Path
|
||||
|
||||
import httpx2
|
||||
import pytest
|
||||
|
||||
from musicmouse.config import WebConfig, load_config
|
||||
from musicmouse.services.web.service import build_app
|
||||
from musicmouse.services.web.settings import to_device_volume, to_percent
|
||||
from musicmouse.simulator.harness import Simulation, build_simulation
|
||||
from tests.conftest import VALID_CONFIG, write_config
|
||||
|
||||
SETTINGS = {
|
||||
"min_volume": 0,
|
||||
"max_volume": 60,
|
||||
"initial_volume": 40,
|
||||
"volume_increment": 5,
|
||||
"button_leds_brightness": 0.5,
|
||||
}
|
||||
|
||||
#: A config file with comments, to prove saving does not flatten them.
|
||||
COMMENTED = """\
|
||||
# The MusicMouse config.
|
||||
general:
|
||||
library:
|
||||
root: music # where the shelves live
|
||||
cache: .cache
|
||||
serial_port: "/dev/ttyUSB0"
|
||||
alsa_device: simulate # no sound from the test suite, please
|
||||
# Volume, 0..100.
|
||||
min_volume: 0
|
||||
max_volume: 60
|
||||
initial_volume: 40
|
||||
|
||||
figures:
|
||||
fuchs:
|
||||
id: "04a1b2c3d4"
|
||||
colors: ["#ff6600", "#ffcc00", "#331100", "wff"]
|
||||
eule:
|
||||
id: "04b2c3d4e5"
|
||||
colors: ["#3355ff", "#66aaff", "#001133", "#ffffff"]
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def sim(config_dir: Path) -> AsyncIterator[Simulation]:
|
||||
config = load_config(write_config(config_dir, VALID_CONFIG))
|
||||
simulation = await build_simulation(config)
|
||||
try:
|
||||
yield simulation
|
||||
finally:
|
||||
await simulation.aclose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(sim: Simulation, config_dir: Path) -> AsyncIterator[httpx2.AsyncClient]:
|
||||
path = config_dir / "config.yml"
|
||||
path.write_text(COMMENTED, encoding="utf-8")
|
||||
api, hub = build_app(sim.app, WebConfig(), path)
|
||||
hub.start()
|
||||
transport = httpx2.ASGITransport(app=api)
|
||||
try:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="http://mouse") as http:
|
||||
yield http
|
||||
finally:
|
||||
hub.stop()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------- mapping
|
||||
|
||||
|
||||
def test_full_percent_is_the_configured_ceiling(config_dir: Path) -> None:
|
||||
general = load_config(write_config(config_dir, VALID_CONFIG)).general
|
||||
assert to_device_volume(100, general) == 60
|
||||
assert to_device_volume(0, general) == 0
|
||||
assert to_device_volume(50, general) == 30
|
||||
|
||||
|
||||
def test_percent_round_trips(config_dir: Path) -> None:
|
||||
general = load_config(write_config(config_dir, VALID_CONFIG)).general
|
||||
for percent in (0, 25, 50, 75, 100):
|
||||
assert to_percent(to_device_volume(percent, general), general) == percent
|
||||
|
||||
|
||||
def test_a_degenerate_range_reads_as_full(config_dir: Path) -> None:
|
||||
"""min == max is a legal config; it must not divide by zero."""
|
||||
general = load_config(write_config(config_dir, VALID_CONFIG)).general
|
||||
general.min_volume = general.max_volume = 40
|
||||
assert to_percent(40, general) == 100
|
||||
assert to_device_volume(50, general) == 40
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------- reading
|
||||
|
||||
|
||||
async def test_settings_expose_only_the_editable_subset(client: httpx2.AsyncClient) -> None:
|
||||
body = (await client.get("/api/settings")).json()
|
||||
assert set(body) == set(SETTINGS)
|
||||
assert body["max_volume"] == 60
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------- writing
|
||||
|
||||
|
||||
async def test_saving_keeps_the_file_readable_and_commented(
|
||||
client: httpx2.AsyncClient, config_dir: Path
|
||||
) -> None:
|
||||
response = await client.put("/api/settings", json={**SETTINGS, "max_volume": 45})
|
||||
assert response.status_code == 200
|
||||
|
||||
text = (config_dir / "config.yml").read_text(encoding="utf-8")
|
||||
assert "max_volume: 45" in text
|
||||
assert "# The MusicMouse config." in text
|
||||
assert "# where the shelves live" in text
|
||||
assert "# Volume, 0..100." in text
|
||||
assert "# no sound from the test suite, please" in text
|
||||
|
||||
# And it still loads.
|
||||
reloaded = load_config(config_dir / "config.yml")
|
||||
assert reloaded.general.max_volume == 45
|
||||
assert set(reloaded.figures) == {"fuchs", "eule"}
|
||||
|
||||
|
||||
async def test_a_new_ceiling_applies_to_the_running_player(
|
||||
client: httpx2.AsyncClient, sim: Simulation
|
||||
) -> None:
|
||||
"""A parent lowering the ceiling expects the next song to be quieter, not the next boot."""
|
||||
await client.post("/api/volume", json={"percent": 100})
|
||||
await sim.bus.drain()
|
||||
assert sim.player.volume == 60
|
||||
|
||||
response = await client.put(
|
||||
"/api/settings", json={**SETTINGS, "max_volume": 30, "initial_volume": 20}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
await sim.bus.drain()
|
||||
|
||||
assert sim.player.volume == 30
|
||||
assert (await client.get("/api/state")).json()["volume"] == 100
|
||||
|
||||
|
||||
async def test_an_inverted_range_is_rejected(client: httpx2.AsyncClient) -> None:
|
||||
response = await client.put("/api/settings", json={**SETTINGS, "min_volume": 70})
|
||||
assert response.status_code == 422
|
||||
assert "min_volume" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_an_initial_volume_outside_the_range_is_rejected(
|
||||
client: httpx2.AsyncClient,
|
||||
) -> None:
|
||||
response = await client.put("/api/settings", json={**SETTINGS, "initial_volume": 90})
|
||||
assert response.status_code == 422
|
||||
assert "initial_volume" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_out_of_bounds_values_are_rejected_by_the_schema(
|
||||
client: httpx2.AsyncClient,
|
||||
) -> None:
|
||||
assert (
|
||||
await client.put("/api/settings", json={**SETTINGS, "button_leds_brightness": 5})
|
||||
).status_code == 422
|
||||
assert (
|
||||
await client.put("/api/settings", json={**SETTINGS, "volume_increment": 0})
|
||||
).status_code == 422
|
||||
|
||||
|
||||
async def test_a_rejected_save_leaves_the_file_alone(
|
||||
client: httpx2.AsyncClient, config_dir: Path
|
||||
) -> None:
|
||||
before = (config_dir / "config.yml").read_text(encoding="utf-8")
|
||||
await client.put("/api/settings", json={**SETTINGS, "min_volume": 70})
|
||||
assert (config_dir / "config.yml").read_text(encoding="utf-8") == before
|
||||
Reference in New Issue
Block a user