"""Unit tests for owner_notify.py's webhook-call shape (#23). Calls _send_now directly rather than going through the background-thread queue, so assertions are synchronous -- the queue itself is just plumbing. """ from datetime import datetime, timezone import pytest import owner_notify class _FakeResponse: def __enter__(self): return self def __exit__(self, *exc): return False def _booking(): return { "booking_id": "BK-1", "client_id": "C-1", "customer_name": "Alice", "customer_contact": "alice@example.com", "service": "Haircut", "start_time": datetime(2026, 8, 10, 9, 0, tzinfo=timezone.utc), "end_time": datetime(2026, 8, 10, 10, 0, tzinfo=timezone.utc), "source": "public", "status": "confirmed", } def test_send_now_skips_when_webhook_url_not_configured(monkeypatch): monkeypatch.setattr(owner_notify, "NOTIFY_WEBHOOK_URL", "") calls = [] monkeypatch.setattr(owner_notify.urllib.request, "urlopen", lambda *a, **kw: calls.append((a, kw)) or _FakeResponse()) owner_notify._send_now({"booking": _booking(), "business_name": "Happy Nails", "notify_channel": "telegram"}) assert calls == [] def test_send_now_posts_json_when_configured(monkeypatch): monkeypatch.setattr(owner_notify, "NOTIFY_WEBHOOK_URL", "https://n8n.example.com/webhook/owner-notify") captured = [] def fake_urlopen(req, timeout=None): captured.append(req) return _FakeResponse() monkeypatch.setattr(owner_notify.urllib.request, "urlopen", fake_urlopen) payload = {"booking": owner_notify._serialize_booking(_booking()), "business_name": "Happy Nails", "notify_channel": "telegram"} owner_notify._send_now(payload) assert len(captured) == 1 req = captured[0] assert req.full_url == "https://n8n.example.com/webhook/owner-notify" assert req.get_header("Content-type") == "application/json" import json body = json.loads(req.data) assert body["booking"]["booking_id"] == "BK-1" assert body["booking"]["start_time"] == "2026-08-10T09:00:00+00:00" assert body["business_name"] == "Happy Nails" assert body["notify_channel"] == "telegram" def test_notify_enqueues_regardless_of_channel(monkeypatch): """Channel routing (only "telegram" sends anything real) lives on the n8n side of this webhook, not in notify() -- an unset/other channel is still queued, not silently dropped here (#23).""" captured = [] monkeypatch.setattr(owner_notify._queue, "put", lambda p: captured.append(p)) client = {"business_name": "Happy Nails", "notify_channel": "email"} owner_notify.notify(client, _booking(), "created") assert len(captured) == 1 assert captured[0]["notify_channel"] == "email" assert captured[0]["business_name"] == "Happy Nails" assert captured[0]["event"] == "created" def test_notify_defaults_business_name_and_channel_when_client_missing(monkeypatch): captured = [] monkeypatch.setattr(owner_notify._queue, "put", lambda p: captured.append(p)) owner_notify.notify(None, _booking(), "cancelled") assert captured[0]["business_name"] == "" assert captured[0]["notify_channel"] is None assert captured[0]["event"] == "cancelled"