Refactor cron to call API via HTTP, capture base_url from browser
CI / test-and-package (push) Successful in 33s
CI / test-and-package (push) Successful in 33s
- meal-cron now runs send_reminders_cron.sh (simple wget loop, no DB mount) - New /api/cron-tick endpoint handles all timezone logic server-side - base_url captured from request.host_url when user saves ntfy settings, stored per-user — no APP_BASE_URL env var needed - Cron endpoint protected by CRON_SECRET shared env var - Removed send_reminders_cron.py, DB volume from cron container - tests: 3 new cron-tick + base_url capture tests (69 total)
This commit is contained in:
@@ -24,7 +24,7 @@ Flask web app for tracking household meal attendance. Users register into househ
|
|||||||
| `Dockerfile` | Python 3.13-alpine. **Must list every `.py` file in the COPY line.** |
|
| `Dockerfile` | Python 3.13-alpine. **Must list every `.py` file in the COPY line.** |
|
||||||
| `docker-compose.yml` | Two services: `meal-tracker` (web) and `meal-cron` (sends reminders). Both on external `caddy` network, shared `meal-data` volume at `/data`. |
|
| `docker-compose.yml` | Two services: `meal-tracker` (web) and `meal-cron` (sends reminders). Both on external `caddy` network, shared `meal-data` volume at `/data`. |
|
||||||
| `watch-for-updates.sh` | Polls git remote, rebuilds on change. Uses `docker compose` with env vars from `.env`. |
|
| `watch-for-updates.sh` | Polls git remote, rebuilds on change. Uses `docker compose` with env vars from `.env`. |
|
||||||
| `send_reminders_cron.py` | Standalone script run every 60s by the `meal-cron` container. Checks per-household local time against 10:00/16:00 triggers. |
|
| `send_reminders_cron.sh` | Simple shell loop run by `meal-cron`: calls `POST /api/cron-tick` on `meal-tracker:5000` every 60s with `X-Cron-Secret` header. No DB access — all logic is server-side. |
|
||||||
| `doc/index.md` | Full project documentation |
|
| `doc/index.md` | Full project documentation |
|
||||||
| `README.md` | Symlink → `doc/index.md` |
|
| `README.md` | Symlink → `doc/index.md` |
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy application
|
# Copy application
|
||||||
COPY app.py models.py auth.py meals.py i18n.py ntfy.py send_reminders_cron.py ./
|
COPY app.py models.py auth.py meals.py i18n.py ntfy.py send_reminders_cron.sh ./
|
||||||
COPY templates/ templates/
|
COPY templates/ templates/
|
||||||
COPY static/ static/
|
COPY static/ static/
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ def create_app() -> Flask:
|
|||||||
def send_reminders_command(date, meal, base_url):
|
def send_reminders_command(date, meal, base_url):
|
||||||
"""Send ntfy meal reminders to all users who have ntfy configured."""
|
"""Send ntfy meal reminders to all users who have ntfy configured."""
|
||||||
target_date = date and __import__("datetime").datetime.strptime(date, "%Y-%m-%d").date() or date.today()
|
target_date = date and __import__("datetime").datetime.strptime(date, "%Y-%m-%d").date() or date.today()
|
||||||
app_base_url = base_url or os.environ.get("APP_BASE_URL", "http://localhost:5000")
|
fallback_url = base_url or os.environ.get("APP_BASE_URL", "http://localhost:5000")
|
||||||
|
|
||||||
meal_types = ["lunch", "dinner"] if meal == "both" else [meal]
|
meal_types = ["lunch", "dinner"] if meal == "both" else [meal]
|
||||||
total_sent = 0
|
total_sent = 0
|
||||||
|
|
||||||
for household in get_all_households():
|
for household in get_all_households():
|
||||||
for user in get_users_with_ntfy(household["id"]):
|
for user in get_users_with_ntfy(household["id"]):
|
||||||
|
user_base_url = user.get("base_url") or fallback_url
|
||||||
for mt in meal_types:
|
for mt in meal_types:
|
||||||
ok = send_meal_reminder(
|
ok = send_meal_reminder(
|
||||||
ntfy_url=user["ntfy_url"],
|
ntfy_url=user["ntfy_url"],
|
||||||
@@ -48,7 +49,7 @@ def create_app() -> Flask:
|
|||||||
callback_token=user["callback_token"],
|
callback_token=user["callback_token"],
|
||||||
meal_type=mt,
|
meal_type=mt,
|
||||||
for_date=target_date,
|
for_date=target_date,
|
||||||
app_base_url=app_base_url,
|
app_base_url=user_base_url,
|
||||||
)
|
)
|
||||||
if ok:
|
if ok:
|
||||||
total_sent += 1
|
total_sent += 1
|
||||||
|
|||||||
+3
-5
@@ -9,6 +9,7 @@ services:
|
|||||||
- meal-data:/data
|
- meal-data:/data
|
||||||
environment:
|
environment:
|
||||||
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
|
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
|
||||||
|
- CRON_SECRET=${CRON_SECRET:-change-me}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
hostname: meal-tracker
|
hostname: meal-tracker
|
||||||
networks:
|
networks:
|
||||||
@@ -18,12 +19,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: ["sh", "-c", "while true; do python3 /app/send_reminders_cron.py; sleep 60; done"]
|
command: ["sh", "/app/send_reminders_cron.sh"]
|
||||||
volumes:
|
|
||||||
- meal-data:/data
|
|
||||||
environment:
|
environment:
|
||||||
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
|
- CRON_SECRET=${CRON_SECRET:-change-me}
|
||||||
- APP_BASE_URL=${APP_BASE_URL:-http://meal-tracker:5000}
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- caddy
|
- caddy
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ from models import (
|
|||||||
get_user_by_callback_token,
|
get_user_by_callback_token,
|
||||||
set_household_timezone,
|
set_household_timezone,
|
||||||
get_household_by_id,
|
get_household_by_id,
|
||||||
|
get_all_households,
|
||||||
|
get_users_with_ntfy,
|
||||||
|
update_reminder_sent,
|
||||||
)
|
)
|
||||||
from i18n import t
|
from i18n import t
|
||||||
from ntfy import send_test_notification
|
from ntfy import send_test_notification, send_meal_reminder
|
||||||
|
import os
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
meals_bp = Blueprint("meals", __name__)
|
meals_bp = Blueprint("meals", __name__)
|
||||||
|
|
||||||
@@ -268,7 +273,10 @@ def ntfy_settings():
|
|||||||
callback_token = ""
|
callback_token = ""
|
||||||
ntfy_token = ""
|
ntfy_token = ""
|
||||||
|
|
||||||
set_user_ntfy(session["user_id"], ntfy_url, ntfy_token, callback_token)
|
# Capture the base URL from the user's browser request
|
||||||
|
base_url = request.host_url.rstrip('/') if ntfy_url else ""
|
||||||
|
|
||||||
|
set_user_ntfy(session["user_id"], ntfy_url, ntfy_token, callback_token, base_url)
|
||||||
flash(t("Ntfy settings saved."), "success")
|
flash(t("Ntfy settings saved."), "success")
|
||||||
return redirect(url_for("meals.dashboard"))
|
return redirect(url_for("meals.dashboard"))
|
||||||
|
|
||||||
@@ -332,3 +340,66 @@ def ntfy_callback():
|
|||||||
|
|
||||||
upsert_response(user["id"], period["id"], status)
|
upsert_response(user["id"], period["id"], status)
|
||||||
return {"message": "Response recorded", "status": status}, 200
|
return {"message": "Response recorded", "status": status}, 200
|
||||||
|
|
||||||
|
|
||||||
|
# ── Cron tick (called by meal-cron container) ─────────────────────────────────
|
||||||
|
|
||||||
|
REMINDER_HOURS = {"lunch": 10, "dinner": 16}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_timezone(tz_str: str) -> ZoneInfo:
|
||||||
|
try:
|
||||||
|
return ZoneInfo(tz_str)
|
||||||
|
except Exception:
|
||||||
|
return ZoneInfo("UTC")
|
||||||
|
|
||||||
|
|
||||||
|
@meals_bp.route("/api/cron-tick", methods=["POST"])
|
||||||
|
def cron_tick():
|
||||||
|
"""Called by the cron container every 60s.
|
||||||
|
|
||||||
|
Checks each household's local time and sends ntfy reminders
|
||||||
|
at 10:00 (lunch) and 16:00 (dinner). Protected by X-Cron-Secret header.
|
||||||
|
"""
|
||||||
|
expected = os.environ.get("CRON_SECRET", "")
|
||||||
|
if expected and request.headers.get("X-Cron-Secret") != expected:
|
||||||
|
return {"error": "unauthorized"}, 403
|
||||||
|
|
||||||
|
now_utc = datetime.now(ZoneInfo("UTC"))
|
||||||
|
sent = 0
|
||||||
|
|
||||||
|
for hh in get_all_households():
|
||||||
|
tz = _resolve_timezone(hh.get("timezone", "UTC"))
|
||||||
|
local_now = now_utc.astimezone(tz)
|
||||||
|
today_str = local_now.strftime("%Y-%m-%d")
|
||||||
|
hour = local_now.hour
|
||||||
|
minute = local_now.minute
|
||||||
|
|
||||||
|
for meal_type, trigger_hour in REMINDER_HOURS.items():
|
||||||
|
if hour != trigger_hour or minute >= 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_col = f"last_{meal_type}_reminder"
|
||||||
|
if hh.get(last_col) == today_str:
|
||||||
|
continue
|
||||||
|
|
||||||
|
users = get_users_with_ntfy(hh["id"])
|
||||||
|
if not users:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
base_url = user.get("base_url", "") or request.host_url.rstrip("/")
|
||||||
|
ok = send_meal_reminder(
|
||||||
|
ntfy_url=user["ntfy_url"],
|
||||||
|
ntfy_token=user["ntfy_token"],
|
||||||
|
callback_token=user["callback_token"],
|
||||||
|
meal_type=meal_type,
|
||||||
|
for_date=local_now.date(),
|
||||||
|
app_base_url=base_url,
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
sent += 1
|
||||||
|
|
||||||
|
update_reminder_sent(hh["id"], meal_type, today_str)
|
||||||
|
|
||||||
|
return {"sent": sent}, 200
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ def init_db() -> None:
|
|||||||
db.execute("ALTER TABLE users ADD COLUMN ntfy_token TEXT DEFAULT ''")
|
db.execute("ALTER TABLE users ADD COLUMN ntfy_token TEXT DEFAULT ''")
|
||||||
if "callback_token" not in cols:
|
if "callback_token" not in cols:
|
||||||
db.execute("ALTER TABLE users ADD COLUMN callback_token TEXT DEFAULT ''")
|
db.execute("ALTER TABLE users ADD COLUMN callback_token TEXT DEFAULT ''")
|
||||||
|
if "base_url" not in cols:
|
||||||
|
db.execute("ALTER TABLE users ADD COLUMN base_url TEXT DEFAULT ''")
|
||||||
|
|
||||||
# Migration: add timezone and reminder tracking to households
|
# Migration: add timezone and reminder tracking to households
|
||||||
hh_cols = [r["name"] for r in db.execute("PRAGMA table_info(households)").fetchall()]
|
hh_cols = [r["name"] for r in db.execute("PRAGMA table_info(households)").fetchall()]
|
||||||
@@ -207,13 +209,13 @@ def get_household_users(household_id: int) -> list[dict]:
|
|||||||
# ── Ntfy / notifications ─────────────────────────────────────────────────────
|
# ── Ntfy / notifications ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
def set_user_ntfy(user_id: int, ntfy_url: str, ntfy_token: str,
|
def set_user_ntfy(user_id: int, ntfy_url: str, ntfy_token: str,
|
||||||
callback_token: str) -> None:
|
callback_token: str, base_url: str = "") -> None:
|
||||||
"""Save ntfy settings for a user."""
|
"""Save ntfy settings for a user."""
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE users SET ntfy_url = ?, ntfy_token = ?, callback_token = ? "
|
"UPDATE users SET ntfy_url = ?, ntfy_token = ?, callback_token = ?, "
|
||||||
"WHERE id = ?",
|
"base_url = ? WHERE id = ?",
|
||||||
(ntfy_url, ntfy_token, callback_token, user_id),
|
(ntfy_url, ntfy_token, callback_token, base_url, user_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@ def get_user_ntfy(user_id: int) -> dict:
|
|||||||
"""Get ntfy settings for a user."""
|
"""Get ntfy settings for a user."""
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
"SELECT ntfy_url, ntfy_token, callback_token FROM users WHERE id = ?",
|
"SELECT ntfy_url, ntfy_token, callback_token, base_url FROM users WHERE id = ?",
|
||||||
(user_id,),
|
(user_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return dict(row) if row else {}
|
return dict(row) if row else {}
|
||||||
@@ -241,7 +243,7 @@ def get_users_with_ntfy(household_id: int) -> list[dict]:
|
|||||||
"""Return all users in a household who have ntfy configured."""
|
"""Return all users in a household who have ntfy configured."""
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
"SELECT id, username, ntfy_url, ntfy_token, callback_token "
|
"SELECT id, username, ntfy_url, ntfy_token, callback_token, base_url "
|
||||||
"FROM users WHERE household_id = ? "
|
"FROM users WHERE household_id = ? "
|
||||||
"AND ntfy_url != '' AND callback_token != '' "
|
"AND ntfy_url != '' AND callback_token != '' "
|
||||||
"ORDER BY username",
|
"ORDER BY username",
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Run by the cron container every ~60s to send meal reminders.
|
|
||||||
|
|
||||||
For each household, checks the current local time. If it's 10:00–10:04,
|
|
||||||
sends lunch reminders; if 16:00–16:04, sends dinner reminders. A
|
|
||||||
per-household sent-date guard prevents duplicate sends.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
from zoneinfo import ZoneInfo, available_timezones
|
|
||||||
|
|
||||||
# Ensure we can import from the app directory
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
from models import get_all_households, get_users_with_ntfy, update_reminder_sent, init_db
|
|
||||||
from ntfy import send_meal_reminder
|
|
||||||
|
|
||||||
APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://meal-tracker:5000")
|
|
||||||
|
|
||||||
# Reminder schedule (local time)
|
|
||||||
REMINDERS = {
|
|
||||||
"lunch": 10, # 10:00
|
|
||||||
"dinner": 16, # 16:00
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_timezone(tz_str: str) -> ZoneInfo:
|
|
||||||
"""Return a ZoneInfo for *tz_str*, falling back to UTC on error."""
|
|
||||||
try:
|
|
||||||
return ZoneInfo(tz_str)
|
|
||||||
except Exception:
|
|
||||||
return ZoneInfo("UTC")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
init_db() # ensure tables exist (first run before web container)
|
|
||||||
now_utc = datetime.now(ZoneInfo("UTC"))
|
|
||||||
sent = 0
|
|
||||||
|
|
||||||
for hh in get_all_households():
|
|
||||||
tz = _resolve_timezone(hh.get("timezone", "UTC"))
|
|
||||||
local_now = now_utc.astimezone(tz)
|
|
||||||
today_str = local_now.strftime("%Y-%m-%d")
|
|
||||||
hour = local_now.hour
|
|
||||||
minute = local_now.minute
|
|
||||||
|
|
||||||
for meal_type, trigger_hour in REMINDERS.items():
|
|
||||||
if hour != trigger_hour or minute >= 5:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_col = f"last_{meal_type}_reminder"
|
|
||||||
if hh.get(last_col) == today_str:
|
|
||||||
continue # already sent today
|
|
||||||
|
|
||||||
users = get_users_with_ntfy(hh["id"])
|
|
||||||
if not users:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
ok = send_meal_reminder(
|
|
||||||
ntfy_url=user["ntfy_url"],
|
|
||||||
ntfy_token=user["ntfy_token"],
|
|
||||||
callback_token=user["callback_token"],
|
|
||||||
meal_type=meal_type,
|
|
||||||
for_date=local_now.date(),
|
|
||||||
app_base_url=APP_BASE_URL,
|
|
||||||
)
|
|
||||||
if ok:
|
|
||||||
print(f"[{local_now:%Y-%m-%d %H:%M}] sent {meal_type} → {user['username']}")
|
|
||||||
sent += 1
|
|
||||||
|
|
||||||
update_reminder_sent(hh["id"], meal_type, today_str)
|
|
||||||
|
|
||||||
if sent:
|
|
||||||
print(f"Sent {sent} reminder(s)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Cron loop — calls the meal-tracker API every 60s to trigger reminders.
|
||||||
|
# The Flask endpoint handles all timezone logic and ntfy sending.
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
wget -q -O- --post-data="" \
|
||||||
|
--header="X-Cron-Secret: ${CRON_SECRET}" \
|
||||||
|
http://meal-tracker:5000/api/cron-tick 2>/dev/null
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
+62
-4
@@ -36,10 +36,33 @@ def _setup_ntfy_user(client, username="ntfyuser", password="testpass",
|
|||||||
import secrets as sec
|
import secrets as sec
|
||||||
token = sec.token_urlsafe(32)
|
token = sec.token_urlsafe(32)
|
||||||
row = db.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
|
row = db.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
|
||||||
set_user_ntfy(row["id"], ntfy_url, ntfy_token, token)
|
set_user_ntfy(row["id"], ntfy_url, ntfy_token, token, "http://localhost")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_ntfy_household(client):
|
||||||
|
"""Register an admin, set timezone, configure ntfy. Returns (client, callback_token)."""
|
||||||
|
client.post("/register", data={
|
||||||
|
"username": "tzadmin",
|
||||||
|
"password": "tzpass123",
|
||||||
|
"confirm": "tzpass123",
|
||||||
|
"household_action": "create",
|
||||||
|
"new_household_name": "TZ Cron Household",
|
||||||
|
}, follow_redirects=True)
|
||||||
|
client.post("/admin/timezone", data={"timezone": "UTC"}, follow_redirects=True)
|
||||||
|
client.post("/ntfy-settings", data={
|
||||||
|
"ntfy_url": "https://ntfy.sh/test-topic",
|
||||||
|
"ntfy_token": "tk_test",
|
||||||
|
}, follow_redirects=True)
|
||||||
|
|
||||||
|
from models import get_db
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT callback_token, base_url FROM users WHERE username = ?", ("tzadmin",)
|
||||||
|
).fetchone()
|
||||||
|
return client, row["callback_token"], row["base_url"]
|
||||||
|
|
||||||
|
|
||||||
class TestNtfyCallback:
|
class TestNtfyCallback:
|
||||||
def test_valid_callback_records_response(self, client):
|
def test_valid_callback_records_response(self, client):
|
||||||
token = _setup_ntfy_user(client)
|
token = _setup_ntfy_user(client)
|
||||||
@@ -164,12 +187,13 @@ class TestNtfySettings:
|
|||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
"SELECT ntfy_url, ntfy_token, callback_token FROM users WHERE username = ?",
|
"SELECT ntfy_url, ntfy_token, callback_token, base_url FROM users WHERE username = ?",
|
||||||
("admin",)
|
("admin",)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
assert row["ntfy_url"] == "https://ntfy.sh/my-alerts"
|
assert row["ntfy_url"] == "https://ntfy.sh/my-alerts"
|
||||||
assert row["ntfy_token"] == "tk_secret"
|
assert row["ntfy_token"] == "tk_secret"
|
||||||
assert len(row["callback_token"]) >= 32 # random token generated
|
assert len(row["callback_token"]) >= 32
|
||||||
|
assert row["base_url"] == "http://localhost" # captured from request # random token generated
|
||||||
|
|
||||||
def test_settings_clears_on_empty_url(self, client):
|
def test_settings_clears_on_empty_url(self, client):
|
||||||
token = _setup_ntfy_user(client)
|
token = _setup_ntfy_user(client)
|
||||||
@@ -191,11 +215,12 @@ class TestNtfySettings:
|
|||||||
}, follow_redirects=True)
|
}, follow_redirects=True)
|
||||||
|
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
"SELECT ntfy_url, callback_token FROM users WHERE username = ?",
|
"SELECT ntfy_url, callback_token, base_url FROM users WHERE username = ?",
|
||||||
("ntfyuser",)
|
("ntfyuser",)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
assert row["ntfy_url"] == ""
|
assert row["ntfy_url"] == ""
|
||||||
assert row["callback_token"] == ""
|
assert row["callback_token"] == ""
|
||||||
|
assert row["base_url"] == ""
|
||||||
|
|
||||||
def test_settings_preserves_callback_token_on_update(self, admin_client):
|
def test_settings_preserves_callback_token_on_update(self, admin_client):
|
||||||
# First save
|
# First save
|
||||||
@@ -291,3 +316,36 @@ class TestTimezoneSettings:
|
|||||||
"timezone": "Europe/Zurich",
|
"timezone": "Europe/Zurich",
|
||||||
})
|
})
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
|
|
||||||
|
|
||||||
|
class TestCronTick:
|
||||||
|
def test_cron_tick_requires_secret(self, client, monkeypatch):
|
||||||
|
monkeypatch.setenv("CRON_SECRET", "my-secret")
|
||||||
|
|
||||||
|
resp = client.post("/api/cron-tick")
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
resp = client.post("/api/cron-tick", headers={"X-Cron-Secret": "wrong"})
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
def test_cron_tick_works_with_secret(self, client, monkeypatch):
|
||||||
|
monkeypatch.setenv("CRON_SECRET", "my-secret")
|
||||||
|
_setup_ntfy_household(client)
|
||||||
|
|
||||||
|
resp = client.post("/api/cron-tick",
|
||||||
|
headers={"X-Cron-Secret": "my-secret"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.get_json()
|
||||||
|
assert "sent" in data
|
||||||
|
|
||||||
|
def test_ntfy_settings_captures_base_url(self, client):
|
||||||
|
"""When saving ntfy settings, the base_url should be captured from the request."""
|
||||||
|
_setup_ntfy_household(client)
|
||||||
|
|
||||||
|
from models import get_db
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT base_url FROM users WHERE username = ?", ("tzadmin",)
|
||||||
|
).fetchone()
|
||||||
|
# In test client, host_url is http://localhost
|
||||||
|
assert row["base_url"] == "http://localhost"
|
||||||
|
|||||||
Reference in New Issue
Block a user