diff --git a/api/crontab_manager.py b/api/crontab_manager.py index 18f7602..5113128 100644 --- a/api/crontab_manager.py +++ b/api/crontab_manager.py @@ -1,5 +1,5 @@ import uuid -from crontab import CronTab +from crontab import CronTab, CronSlices class CrontabManager: @@ -39,6 +39,9 @@ class CrontabManager: command: str, is_enabled: bool = True, ): + if not CronSlices.is_valid(cron_expression): + raise ValueError(f"Invalid cron expression: {cron_expression!r}") + if not alarm_id: alarm_id = str(uuid.uuid4()) diff --git a/tests/test_api.py b/tests/test_api.py index 05eabef..1ba884b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -164,6 +164,22 @@ def test_set_alarm_rejects_custom_command(): assert "command" in str(res.json()["errors"]) +def test_set_alarm_rejects_invalid_cron_expression(): + """Invalid cron expressions must be rejected at the API boundary.""" + headers = {"X-API-Key": "test-secret"} + mutation = """ + mutation { + setAlarm(cronExpression: "definitely not valid") { + id + } + } + """ + res = client.post("/graphql", json={"query": mutation}, headers=headers) + assert res.status_code == 200 + assert "errors" in res.json() + assert "Invalid cron expression" in str(res.json()["errors"]) + + def test_graphql_workflow(): headers = {"X-API-Key": "test-secret"} diff --git a/tests/test_crontab.py b/tests/test_crontab.py index 0bbb89e..8d97236 100644 --- a/tests/test_crontab.py +++ b/tests/test_crontab.py @@ -66,3 +66,9 @@ def test_delete_alarm(crontab_file): manager.delete_alarm(alarm_id) assert len(manager.get_alarms()) == 0 + + +def test_set_alarm_rejects_invalid_cron_expression(crontab_file): + manager = CrontabManager(tabfile=crontab_file) + with pytest.raises(ValueError, match="Invalid cron expression"): + manager.set_alarm("test-id", "not-a-cron-expression", "cmd", True)