Add local settings management

Add cellular-first ntfy delivery with WiFi fallback and safe SIM868 HTTP header framing.

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-09 21:26:15 -04:00
co-authored by Copilot
parent b77ed838bd
commit 3a6f12ba9d
15 changed files with 1118 additions and 103 deletions
+116
View File
@@ -139,6 +139,122 @@ class NtfyTransportTest(unittest.TestCase):
)
response.raise_for_status.assert_called_once_with()
@patch("modules.app_alerts.app_cellular.http_post")
@patch(
"modules.app_alerts.app_cellular.is_configured",
return_value=True,
)
@patch("modules.app_alerts.requests.post")
def test_cellular_is_primary_when_configured(
self,
post,
_is_configured,
cellular_post,
):
cellular_post.return_value = (200, b"ok")
app_alerts.send_ntfy(
"https://ntfy.example/topic",
"secret",
"Alert title",
"Alert body",
"high",
["warning"],
)
cellular_post.assert_called_once_with(
"https://ntfy.example/topic",
b"Alert body",
{
"Title": "Alert title",
"Priority": "high",
"Tags": "warning",
"Authorization": "Bearer secret",
},
content_type="text/plain; charset=utf-8",
)
post.assert_not_called()
@patch("modules.app_alerts.app_cellular.http_post")
@patch(
"modules.app_alerts.app_cellular.is_configured",
return_value=True,
)
@patch("modules.app_alerts.requests.post")
def test_cellular_http_error_falls_back_to_wifi(
self,
post,
_is_configured,
cellular_post,
):
cellular_post.return_value = (503, b"error")
post.return_value.raise_for_status.return_value = None
app_alerts.send_ntfy(
"https://ntfy.example/topic",
"",
"Alert title",
"Alert body",
)
cellular_post.assert_called_once()
post.assert_called_once()
@patch("modules.app_alerts.app_cellular.http_post")
@patch(
"modules.app_alerts.app_cellular.is_configured",
return_value=True,
)
@patch("modules.app_alerts.requests.post")
def test_wifi_error_is_reported_after_cellular_failure(
self,
post,
_is_configured,
cellular_post,
):
cellular_post.side_effect = app_alerts.app_cellular.CellularError(
"modem offline"
)
post.side_effect = requests.ReadTimeout("response timed out")
with self.assertRaises(requests.ReadTimeout):
app_alerts.send_ntfy(
"https://ntfy.example/topic",
"",
"Alert title",
"Alert body",
)
cellular_post.assert_called_once()
@patch("modules.app_alerts.app_cellular.http_post")
@patch(
"modules.app_alerts.app_cellular.is_configured",
return_value=True,
)
@patch("modules.app_alerts.requests.post")
def test_both_transport_failures_do_not_expose_private_url(
self,
post,
_is_configured,
cellular_post,
):
post.side_effect = requests.ConnectionError("offline")
cellular_post.return_value = (503, b"error")
with self.assertRaises(requests.ConnectionError) as raised:
app_alerts.send_ntfy(
"https://ntfy.example/private-topic",
"",
"Alert title",
"Alert body",
)
self.assertEqual(
app_alerts._transport_error(raised.exception),
"Notification connection failed",
)
def test_transport_error_does_not_expose_url(self):
response = requests.Response()
response.status_code = 401