"""Tests for ntfy integration — callback endpoint, settings, and CLI.""" import json import secrets from datetime import date, timedelta from models import get_db, set_user_ntfy, get_user_ntfy def _setup_ntfy_user(client, username="ntfyuser", password="testpass", ntfy_url="https://ntfy.sh/test-topic", ntfy_token="tk_test123"): """Register a user and save ntfy settings. Returns callback_token.""" # Admin creates household and registers client.post("/register", data={ "username": "admin2", "password": "admin2pass", "confirm": "admin2pass", "household_action": "create", "new_household_name": "Ntfy Household", }, follow_redirects=True) client.get("/logout") from models import get_household_by_name hh = get_household_by_name("Ntfy Household") client.post("/register", data={ "username": username, "password": password, "confirm": password, "household_action": "join", "household_id": str(hh["id"]), }, follow_redirects=True) # User is now logged in # Save ntfy settings db = get_db() import secrets as sec token = sec.token_urlsafe(32) row = db.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone() set_user_ntfy(row["id"], ntfy_url, ntfy_token, token) return token class TestNtfyCallback: def test_valid_callback_records_response(self, client): token = _setup_ntfy_user(client) today = date.today().isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": today, "status": "yes"}), content_type="application/json") assert resp.status_code == 200 data = resp.get_json() assert data["message"] == "Response recorded" assert data["status"] == "yes" def test_callback_records_no_status(self, client): token = _setup_ntfy_user(client) today = date.today().isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "dinner", "date": today, "status": "no"}), content_type="application/json") assert resp.status_code == 200 assert resp.get_json()["status"] == "no" def test_callback_changes_mind(self, client): token = _setup_ntfy_user(client) today = date.today().isoformat() # First response client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": today, "status": "yes"}), content_type="application/json") # Change mind resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": today, "status": "no"}), content_type="application/json") assert resp.status_code == 200 assert resp.get_json()["status"] == "no" def test_invalid_token_returns_403(self, client): today = date.today().isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": "not-a-valid-token", "meal_type": "lunch", "date": today, "status": "yes"}), content_type="application/json") assert resp.status_code == 403 assert resp.get_json()["error"] == "Invalid token" def test_past_date_returns_400(self, client): token = _setup_ntfy_user(client) yesterday = (date.today() - timedelta(days=1)).isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": yesterday, "status": "yes"}), content_type="application/json") assert resp.status_code == 400 assert "past" in resp.get_json()["error"].lower() def test_invalid_meal_type_returns_400(self, client): token = _setup_ntfy_user(client) today = date.today().isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "breakfast", "date": today, "status": "yes"}), content_type="application/json") assert resp.status_code == 400 assert "meal_type" in resp.get_json()["error"].lower() def test_invalid_status_returns_400(self, client): token = _setup_ntfy_user(client) today = date.today().isoformat() resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": today, "status": "maybe"}), content_type="application/json") assert resp.status_code == 400 assert "status" in resp.get_json()["error"].lower() def test_invalid_date_format_returns_400(self, client): token = _setup_ntfy_user(client) resp = client.post("/api/ntfy-callback", data=json.dumps({"token": token, "meal_type": "lunch", "date": "not-a-date", "status": "yes"}), content_type="application/json") assert resp.status_code == 400 def test_missing_json_returns_400(self, client): resp = client.post("/api/ntfy-callback", data="not json", content_type="text/plain") assert resp.status_code == 400 def test_empty_body_returns_400(self, client): resp = client.post("/api/ntfy-callback", data=None, content_type="application/json") assert resp.status_code == 400 class TestNtfySettings: def test_settings_requires_login(self, client): resp = client.post("/ntfy-settings", data={ "ntfy_url": "https://ntfy.sh/test", "ntfy_token": "", }) assert resp.status_code == 302 # redirects to login def test_settings_saves_url_and_generates_callback_token(self, admin_client): resp = admin_client.post("/ntfy-settings", data={ "ntfy_url": "https://ntfy.sh/my-alerts", "ntfy_token": "tk_secret", }, follow_redirects=True) assert resp.status_code == 200 db = get_db() row = db.execute( "SELECT ntfy_url, ntfy_token, callback_token FROM users WHERE username = ?", ("admin",) ).fetchone() assert row["ntfy_url"] == "https://ntfy.sh/my-alerts" assert row["ntfy_token"] == "tk_secret" assert len(row["callback_token"]) >= 32 # random token generated def test_settings_clears_on_empty_url(self, client): token = _setup_ntfy_user(client) # User is logged in as ntfyuser # Verify settings exist db = get_db() row = db.execute( "SELECT ntfy_url, callback_token FROM users WHERE username = ?", ("ntfyuser",) ).fetchone() assert row["ntfy_url"] != "" assert row["callback_token"] != "" # Clear client.post("/ntfy-settings", data={ "ntfy_url": "", "ntfy_token": "", }, follow_redirects=True) row = db.execute( "SELECT ntfy_url, callback_token FROM users WHERE username = ?", ("ntfyuser",) ).fetchone() assert row["ntfy_url"] == "" assert row["callback_token"] == "" def test_settings_preserves_callback_token_on_update(self, admin_client): # First save admin_client.post("/ntfy-settings", data={ "ntfy_url": "https://ntfy.sh/initial", "ntfy_token": "tk_old", }, follow_redirects=True) db = get_db() row = db.execute( "SELECT callback_token FROM users WHERE username = ?", ("admin",) ).fetchone() first_token = row["callback_token"] # Update URL, token should stay same admin_client.post("/ntfy-settings", data={ "ntfy_url": "https://ntfy.sh/updated", "ntfy_token": "tk_new", }, follow_redirects=True) row = db.execute( "SELECT ntfy_url, callback_token FROM users WHERE username = ?", ("admin",) ).fetchone() assert row["ntfy_url"] == "https://ntfy.sh/updated" assert row["callback_token"] == first_token # unchanged class TestNtfyTestEndpoint: def test_test_requires_login(self, client): resp = client.post("/ntfy-test", data={ "ntfy_url": "https://ntfy.sh/test", }) assert resp.status_code == 302