"""The IR remote's number-key mapping, over the REST API.""" from __future__ import annotations from collections.abc import AsyncIterator from pathlib import Path import httpx2 import pytest from musicmouse.config import RemoteSlotConfig, WebConfig, load_config from musicmouse.services.web.service import build_app from musicmouse.simulator.harness import Simulation, build_simulation from tests.conftest import VALID_CONFIG, write_config #: A config file with comments, to prove saving does not flatten them. COMMENTED = """\ # The MusicMouse config. general: library: root: music cache: .cache serial_port: "/dev/ttyUSB0" alsa_device: simulate 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() await api.state.ha_client.aclose() async def album_id(sim: Simulation, title: str) -> str: return next(a.id for a in sim.app.library.albums if a.title == title) # ------------------------------------------------------------------------------ lirc async def test_lirc_is_a_404_when_unconfigured(client: httpx2.AsyncClient) -> None: assert (await client.get("/api/lirc")).status_code == 404 # --------------------------------------------------------------------------- reading async def test_the_mapping_is_empty_by_default(client: httpx2.AsyncClient) -> None: body = (await client.get("/api/remote/mapping")).json() assert body["slots"] == [] # --------------------------------------------------------------------------- writing async def test_saving_an_album_slot_round_trips( client: httpx2.AsyncClient, sim: Simulation, config_dir: Path ) -> None: target = await album_id(sim, "Kinderparty Lieder") response = await client.put( "/api/remote/mapping", json={"slots": {"3": {"target_kind": "album", "target": target}}} ) assert response.status_code == 200 slots = response.json()["slots"] assert slots == [ { "digit": "3", "target_kind": "album", "target": target, "resolved_album_id": target, "track_index": 0, } ] text = (config_dir / "config.yml").read_text(encoding="utf-8") assert "# The MusicMouse config." in text # comments survive reloaded = load_config(config_dir / "config.yml") assert reloaded.remote["3"].target == target assert "track_index" not in text # a whole-album slot reads as it always did async def test_saving_a_song_slot_round_trips( client: httpx2.AsyncClient, sim: Simulation, config_dir: Path ) -> None: target = await album_id(sim, "Kinderparty Lieder") response = await client.put( "/api/remote/mapping", json={"slots": {"4": {"target_kind": "album", "target": target, "track_index": 1}}}, ) assert response.status_code == 200 assert response.json()["slots"][0]["track_index"] == 1 assert (await client.get("/api/remote/mapping")).json()["slots"][0]["track_index"] == 1 assert sim.app.config.remote["4"].track_index == 1 reloaded = load_config(config_dir / "config.yml") assert reloaded.remote["4"].track_index == 1 async def test_a_song_past_the_end_of_the_album_is_rejected( client: httpx2.AsyncClient, sim: Simulation ) -> None: target = await album_id(sim, "Kinderparty Lieder") response = await client.put( "/api/remote/mapping", json={"slots": {"4": {"target_kind": "album", "target": target, "track_index": 2}}}, ) assert response.status_code == 422 assert "no track 2" in response.json()["detail"] async def test_a_song_of_a_series_is_rejected(client: httpx2.AsyncClient) -> None: response = await client.put( "/api/remote/mapping", json={ "slots": { "7": {"target_kind": "series", "target": "Wissen macht Ah", "track_index": 1} } }, ) assert response.status_code == 422 # --------------------------------------------------------------------------- pressing async def test_pressing_a_key_plays_its_song_and_then_the_rest( client: httpx2.AsyncClient, sim: Simulation ) -> None: target = await album_id(sim, "Kinderparty Lieder") sim.app.config.remote = { "4": RemoteSlotConfig(target_kind="album", target=target, track_index=1) } response = await client.post("/api/remote/press/4") assert response.status_code == 204 await sim.bus.drain() assert sim.player.playlist is not None assert sim.player.playlist.album_id == target assert len(sim.player.playlist) == 2 # the whole album, not just the one song assert sim.player.track_index == 1 async def test_pressing_an_unassigned_key_does_nothing( client: httpx2.AsyncClient, sim: Simulation ) -> None: assert (await client.post("/api/remote/press/5")).status_code == 204 await sim.bus.drain() assert sim.player.playlist is None async def test_pressing_a_non_digit_is_rejected(client: httpx2.AsyncClient) -> None: assert (await client.post("/api/remote/press/x")).status_code == 422 async def test_saving_a_series_slot_resolves_to_the_latest_episode( client: httpx2.AsyncClient, ) -> None: response = await client.put( "/api/remote/mapping", json={"slots": {"7": {"target_kind": "series", "target": "Wissen macht Ah"}}}, ) assert response.status_code == 200 slot = response.json()["slots"][0] assert slot["target_kind"] == "series" assert slot["target"] == "Wissen macht Ah" assert slot["resolved_album_id"] is not None async def test_saving_clears_digits_left_out( client: httpx2.AsyncClient, sim: Simulation ) -> None: target = await album_id(sim, "Kinderparty Lieder") await client.put( "/api/remote/mapping", json={"slots": {"1": {"target_kind": "album", "target": target}}} ) response = await client.put("/api/remote/mapping", json={"slots": {}}) assert response.json()["slots"] == [] async def test_an_unresolvable_album_target_is_rejected(client: httpx2.AsyncClient) -> None: response = await client.put( "/api/remote/mapping", json={"slots": {"1": {"target_kind": "album", "target": "no-such-album"}}}, ) assert response.status_code == 422 assert "1" in response.json()["detail"] async def test_an_unresolvable_series_target_is_rejected(client: httpx2.AsyncClient) -> None: response = await client.put( "/api/remote/mapping", json={"slots": {"2": {"target_kind": "series", "target": "no such show"}}}, ) assert response.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/remote/mapping", json={"slots": {"1": {"target_kind": "album", "target": "no-such-album"}}}, ) assert (config_dir / "config.yml").read_text(encoding="utf-8") == before