feat: pass alarm style through crontab manager and GraphQL API

- crontab_manager.get_alarms now parses --style from the cron command
  (defaults to 'blink' for legacy entries without --style)
- GraphQL Alarm type gains a 'style' field
- setAlarm accepts a 'style' arg (default 'simple') and validates it
  against the styles registry; unknown styles raise a GraphQL error
- _default_command embeds --style <name> into the cron command
- startRinging accepts 'style' (default 'simple') and passes --style to
  the spawned wecker.py

The crontab remains the single source of truth; the style simply rides
in the cron command alongside --music-file.
This commit is contained in:
2026-07-31 16:20:16 +02:00
parent b3f3db6f13
commit c50f296232
4 changed files with 177 additions and 7 deletions
+105
View File
@@ -2,6 +2,7 @@ from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
import tempfile
import os
import pytest
import signal
# We need to set the environment variable before importing the app
@@ -403,3 +404,107 @@ def test_pid_file_same_shared_constant_in_api():
assert api_schema.PID_FILE is common.PID_FILE, (
"api.schema.PID_FILE must reference common.PID_FILE, not redefine it"
)
def _cleanup_alarm(alarm_id):
headers = {"X-API-Key": "test-secret"}
client.post(
"/graphql",
json={"query": f'mutation {{ deleteAlarm(id: "{alarm_id}") }}'},
headers=headers,
)
def test_set_alarm_accepts_and_returns_style():
headers = {"X-API-Key": "test-secret"}
mutation = '''
mutation {
setAlarm(cronExpression: "0 9 * * *", style: "blink") {
id
style
}
}
'''
res = client.post("/graphql", json={"query": mutation}, headers=headers)
assert res.status_code == 200
alarm = res.json()["data"]["setAlarm"]
assert alarm["style"] == "blink"
_cleanup_alarm(alarm["id"])
def test_set_alarm_default_style_is_simple():
headers = {"X-API-Key": "test-secret"}
mutation = '''
mutation {
setAlarm(cronExpression: "0 9 * * *") { id style }
}
'''
res = client.post("/graphql", json={"query": mutation}, headers=headers)
assert res.status_code == 200
alarm = res.json()["data"]["setAlarm"]
assert alarm["style"] == "simple"
_cleanup_alarm(alarm["id"])
def test_set_alarm_rejects_unknown_style():
headers = {"X-API-Key": "test-secret"}
mutation = '''
mutation {
setAlarm(cronExpression: "0 9 * * *", style: "nope") { id }
}
'''
res = client.post("/graphql", json={"query": mutation}, headers=headers)
assert res.status_code == 200
assert "errors" in res.json()
assert "Unknown alarm style" in str(res.json()["errors"])
def test_get_alarms_includes_style():
headers = {"X-API-Key": "test-secret"}
set_mut = '''
mutation { setAlarm(cronExpression: "0 9 * * *", style: "blink") { id } }
'''
res = client.post("/graphql", json={"query": set_mut}, headers=headers)
alarm_id = res.json()["data"]["setAlarm"]["id"]
res = client.post(
"/graphql", json={"query": "{ getAlarms { id style } }"}, headers=headers
)
alarms = res.json()["data"]["getAlarms"]
assert any(a["id"] == alarm_id and a["style"] == "blink" for a in alarms)
_cleanup_alarm(alarm_id)
def test_start_ringing_passes_style():
from api.schema import Mutation
mutation = Mutation()
with patch("api.schema.is_wecker_ringing", return_value=False), \
patch("subprocess.Popen") as mock_popen:
mock_popen.return_value.pid = 9999
assert mutation.start_ringing(style="blink") is True
argv = mock_popen.call_args[0][0]
assert "--style" in argv and "blink" in argv
def test_start_ringing_default_style_is_simple():
from api.schema import Mutation
mutation = Mutation()
with patch("api.schema.is_wecker_ringing", return_value=False), \
patch("subprocess.Popen") as mock_popen:
mock_popen.return_value.pid = 9999
mutation.start_ringing()
argv = mock_popen.call_args[0][0]
assert "simple" in argv
def test_start_ringing_rejects_unknown_style():
from api.schema import Mutation
mutation = Mutation()
with patch("api.schema.is_wecker_ringing", return_value=False), \
patch("subprocess.Popen") as mock_popen:
with pytest.raises(ValueError, match="Unknown alarm style"):
mutation.start_ringing(style="nope")
mock_popen.assert_not_called()