mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
feat: add appointments, campaigns, admin, storage, tests and various updates
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
554
tests/test_appointments.py
Normal file
554
tests/test_appointments.py
Normal file
@@ -0,0 +1,554 @@
|
||||
"""
|
||||
Tests for appointment endpoints:
|
||||
GET /api/v1/appointments
|
||||
PATCH /api/v1/appointments/{id}
|
||||
GET /api/v1/appointments/chatbot/{chatbot_id}/hours
|
||||
PUT /api/v1/appointments/chatbot/{chatbot_id}/hours
|
||||
GET /api/v1/chatbots/{chatbot_id}/booking-info (public)
|
||||
GET /api/v1/chatbots/{chatbot_id}/available-slots (public)
|
||||
POST /api/v1/chatbots/{chatbot_id}/appointments (public)
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def make_user(uid="user-1"):
|
||||
u = MagicMock()
|
||||
u.id = uid
|
||||
u.email = "user@example.com"
|
||||
return u
|
||||
|
||||
|
||||
SAMPLE_APPT = {
|
||||
"id": "appt-1",
|
||||
"chatbot_id": "chatbot-1",
|
||||
"conversation_id": None,
|
||||
"customer_name": "Alice",
|
||||
"customer_contact": "alice@example.com",
|
||||
"service": "Consultation",
|
||||
"slot_start": "2099-06-10T09:00:00",
|
||||
"slot_end": "2099-06-10T10:00:00",
|
||||
"status": "pending",
|
||||
"notes": None,
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
}
|
||||
|
||||
SAMPLE_HOURS = {
|
||||
"id": "bh-1",
|
||||
"chatbot_id": "chatbot-1",
|
||||
"day_of_week": 0, # Monday
|
||||
"is_open": True,
|
||||
"open_time": "09:00",
|
||||
"close_time": "17:00",
|
||||
"slot_duration_minutes": 60,
|
||||
}
|
||||
|
||||
|
||||
def make_supabase(plan="starter", company_id="company-1",
|
||||
appointments=None, appointment=None,
|
||||
hours=None, chatbot_booking=True):
|
||||
sb = MagicMock()
|
||||
|
||||
def table_side(name):
|
||||
t = MagicMock()
|
||||
for m in ("select", "insert", "update", "delete", "eq", "neq",
|
||||
"in_", "order", "range", "limit", "gte", "lt"):
|
||||
getattr(t, m).return_value = t
|
||||
|
||||
if name == "subscriptions":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{"plan": plan}]))
|
||||
elif name == "companies":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{"id": company_id}]))
|
||||
elif name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{
|
||||
"id": "chatbot-1", "company_id": company_id,
|
||||
"booking_enabled": chatbot_booking,
|
||||
"is_published": True,
|
||||
}]))
|
||||
elif name == "appointments":
|
||||
if appointment is not None:
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[appointment]))
|
||||
else:
|
||||
t.execute = MagicMock(return_value=MagicMock(
|
||||
data=appointments if appointments is not None else [SAMPLE_APPT]
|
||||
))
|
||||
elif name == "business_hours":
|
||||
rows = hours if hours is not None else [SAMPLE_HOURS]
|
||||
t.execute = MagicMock(return_value=MagicMock(data=rows))
|
||||
else:
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
|
||||
sb.table.side_effect = table_side
|
||||
return sb
|
||||
|
||||
|
||||
# ── Tests: list appointments ───────────────────────────────────────────────────
|
||||
|
||||
class TestListAppointments:
|
||||
def test_requires_auth(self, client):
|
||||
resp = client.get("/api/v1/appointments")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_free_plan_returns_402(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="free")
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/appointments",
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 402
|
||||
|
||||
def test_returns_appointment_list(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter")
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/appointments",
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["customer_name"] == "Alice"
|
||||
assert data[0]["status"] == "pending"
|
||||
|
||||
def test_returns_empty_when_no_chatbots(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter")
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/appointments",
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
|
||||
# ── Tests: update appointment status ──────────────────────────────────────────
|
||||
|
||||
class TestUpdateAppointmentStatus:
|
||||
def _owned_appt(self, company_id="company-1", status="pending"):
|
||||
return {**SAMPLE_APPT, "status": status,
|
||||
"chatbots": {"company_id": company_id}}
|
||||
|
||||
def test_requires_auth(self, client):
|
||||
resp = client.patch("/api/v1/appointments/appt-1",
|
||||
json={"status": "confirmed"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_valid_status_transitions(self, client):
|
||||
for new_status in ("pending", "confirmed", "cancelled", "completed"):
|
||||
user = make_user()
|
||||
appt = self._owned_appt()
|
||||
updated = {**SAMPLE_APPT, "status": new_status}
|
||||
|
||||
call_count = {"n": 0}
|
||||
def table_side(name, _ns=new_status):
|
||||
t = MagicMock()
|
||||
for m in ("select", "insert", "update", "delete",
|
||||
"eq", "neq", "in_", "order", "range", "limit"):
|
||||
getattr(t, m).return_value = t
|
||||
if name == "subscriptions":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{"plan": "starter"}]))
|
||||
elif name == "companies":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{"id": "company-1"}]))
|
||||
elif name == "appointments":
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] == 1:
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[appt]))
|
||||
else:
|
||||
t.execute = MagicMock(return_value=MagicMock(
|
||||
data=[{**SAMPLE_APPT, "status": _ns}]))
|
||||
elif name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[
|
||||
{"id": "chatbot-1", "company_id": "company-1"}
|
||||
]))
|
||||
else:
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
|
||||
sb = MagicMock()
|
||||
sb.table.side_effect = table_side
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.patch("/api/v1/appointments/appt-1",
|
||||
json={"status": new_status},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200, f"Failed for status={new_status}"
|
||||
|
||||
def test_invalid_status_returns_400(self, client):
|
||||
user = make_user()
|
||||
appt = self._owned_appt()
|
||||
sb = make_supabase(plan="starter", appointment=appt)
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.patch("/api/v1/appointments/appt-1",
|
||||
json={"status": "flying"},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_403_for_other_companys_appointment(self, client):
|
||||
user = make_user()
|
||||
appt = self._owned_appt(company_id="company-OTHER")
|
||||
sb = make_supabase(plan="starter", company_id="company-1", appointment=appt)
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.patch("/api/v1/appointments/appt-1",
|
||||
json={"status": "confirmed"},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_404_when_not_found(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter")
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "appointments":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.patch("/api/v1/appointments/nonexistent",
|
||||
json={"status": "confirmed"},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ── Tests: business hours ──────────────────────────────────────────────────────
|
||||
|
||||
class TestBusinessHours:
|
||||
def test_get_hours_requires_auth(self, client):
|
||||
resp = client.get("/api/v1/appointments/chatbot/chatbot-1/hours")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_get_hours_returns_list(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter")
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/appointments/chatbot/chatbot-1/hours",
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert data[0]["day_of_week"] == 0
|
||||
assert data[0]["open_time"] == "09:00"
|
||||
|
||||
def test_save_hours_requires_auth(self, client):
|
||||
resp = client.put("/api/v1/appointments/chatbot/chatbot-1/hours",
|
||||
json={"hours": []})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_save_hours_success(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter")
|
||||
hours_payload = [
|
||||
{"day_of_week": 0, "is_open": True, "open_time": "09:00",
|
||||
"close_time": "17:00", "slot_duration_minutes": 60},
|
||||
{"day_of_week": 6, "is_open": False, "open_time": "09:00",
|
||||
"close_time": "17:00", "slot_duration_minutes": 60},
|
||||
]
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.put("/api/v1/appointments/chatbot/chatbot-1/hours",
|
||||
json={"hours": hours_payload},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["success"] is True
|
||||
|
||||
def test_save_hours_free_plan_returns_402(self, client):
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="free")
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.put("/api/v1/appointments/chatbot/chatbot-1/hours",
|
||||
json={"hours": []},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 402
|
||||
|
||||
def test_save_hours_inserts_when_not_exists(self, client):
|
||||
"""When no existing row, should INSERT."""
|
||||
user = make_user()
|
||||
sb = make_supabase(plan="starter", hours=[]) # no existing hours
|
||||
insert_calls = []
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "business_hours":
|
||||
orig_insert = t.insert
|
||||
def track_insert(data):
|
||||
insert_calls.append(data)
|
||||
return t
|
||||
t.insert = track_insert
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_current_user", return_value=user), \
|
||||
patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.put("/api/v1/appointments/chatbot/chatbot-1/hours",
|
||||
json={"hours": [
|
||||
{"day_of_week": 1, "is_open": True,
|
||||
"open_time": "08:00", "close_time": "16:00",
|
||||
"slot_duration_minutes": 30}
|
||||
]},
|
||||
headers={"Authorization": "Bearer tok"})
|
||||
assert resp.status_code == 200
|
||||
assert len(insert_calls) == 1
|
||||
assert insert_calls[0]["day_of_week"] == 1
|
||||
assert "id" in insert_calls[0]
|
||||
|
||||
|
||||
# ── Tests: public booking-info ─────────────────────────────────────────────────
|
||||
|
||||
class TestPublicBookingInfo:
|
||||
def test_returns_booking_info(self, client):
|
||||
sb = make_supabase(chatbot_booking=True)
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{
|
||||
"id": "chatbot-1",
|
||||
"name": "Support Bot",
|
||||
"booking_enabled": True,
|
||||
"companies": {"name": "ACME Corp"},
|
||||
}]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/booking-info")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["chatbot_name"] == "Support Bot"
|
||||
assert body["company_name"] == "ACME Corp"
|
||||
assert body["chatbot_id"] == "chatbot-1"
|
||||
|
||||
def test_404_when_chatbot_not_found(self, client):
|
||||
sb = make_supabase()
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/nonexistent/booking-info")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_400_when_booking_disabled(self, client):
|
||||
sb = make_supabase(chatbot_booking=False)
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{
|
||||
"id": "chatbot-1", "name": "Bot", "booking_enabled": False,
|
||||
"companies": {"name": "ACME"},
|
||||
}]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/booking-info")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
# ── Tests: public available-slots ─────────────────────────────────────────────
|
||||
|
||||
class TestAvailableSlots:
|
||||
def test_returns_slots_for_open_day(self, client):
|
||||
sb = make_supabase(chatbot_booking=True, hours=[SAMPLE_HOURS])
|
||||
# No booked appointments
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "appointments":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
# Use a far-future Monday so none are "past"
|
||||
future_monday = "2099-06-09" # a Monday
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get(f"/api/v1/chatbots/chatbot-1/available-slots?date={future_monday}")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["date"] == future_monday
|
||||
# 09:00-17:00 with 60-min slots = 8 slots
|
||||
assert len(body["slots"]) == 8
|
||||
|
||||
def test_returns_empty_for_closed_day(self, client):
|
||||
closed_hours = {**SAMPLE_HOURS, "is_open": False}
|
||||
sb = make_supabase(chatbot_booking=True, hours=[closed_hours])
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=2099-06-09")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["slots"] == []
|
||||
|
||||
def test_returns_empty_when_no_hours_configured(self, client):
|
||||
sb = make_supabase(chatbot_booking=True, hours=[])
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=2099-06-09")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["slots"] == []
|
||||
|
||||
def test_booked_slots_excluded(self, client):
|
||||
"""A slot that is already booked should not appear."""
|
||||
sb = make_supabase(chatbot_booking=True, hours=[SAMPLE_HOURS])
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "appointments":
|
||||
# 09:00 is already booked
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[
|
||||
{"slot_start": "2099-06-09T09:00:00",
|
||||
"slot_end": "2099-06-09T10:00:00"}
|
||||
]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=2099-06-09")
|
||||
assert resp.status_code == 200
|
||||
slots = resp.json()["slots"]
|
||||
starts = [s["slot_start"] for s in slots]
|
||||
assert not any("T09:00:00" in s for s in starts)
|
||||
|
||||
def test_invalid_date_format_returns_400(self, client):
|
||||
sb = make_supabase(chatbot_booking=True)
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=not-a-date")
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_booking_disabled_returns_400(self, client):
|
||||
sb = make_supabase(chatbot_booking=False)
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[
|
||||
{"id": "chatbot-1", "booking_enabled": False, "is_published": True}
|
||||
]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=2099-06-09")
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_30_min_slots_yield_correct_count(self, client):
|
||||
"""09:00-11:00 with 30-min slots should yield 4 slots."""
|
||||
short_hours = {**SAMPLE_HOURS, "close_time": "11:00", "slot_duration_minutes": 30}
|
||||
sb = make_supabase(chatbot_booking=True, hours=[short_hours])
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "appointments":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.get("/api/v1/chatbots/chatbot-1/available-slots?date=2099-06-09")
|
||||
assert len(resp.json()["slots"]) == 4
|
||||
|
||||
|
||||
# ── Tests: public create appointment ──────────────────────────────────────────
|
||||
|
||||
class TestCreateAppointment:
|
||||
def _sb_with_open_slot(self, slot_start="2099-06-09T09:00:00"):
|
||||
"""Supabase mock that returns one available slot matching slot_start."""
|
||||
insert_result = {**SAMPLE_APPT, "slot_start": slot_start}
|
||||
call_count = {"n": 0}
|
||||
sb = MagicMock()
|
||||
|
||||
def table_side(name):
|
||||
t = MagicMock()
|
||||
for m in ("select", "insert", "update", "delete", "eq", "neq",
|
||||
"in_", "order", "range", "limit", "gte", "lt"):
|
||||
getattr(t, m).return_value = t
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[{
|
||||
"id": "chatbot-1", "booking_enabled": True, "is_published": True,
|
||||
}]))
|
||||
elif name == "business_hours":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[SAMPLE_HOURS]))
|
||||
elif name == "appointments":
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] == 1:
|
||||
# _get_available_slots: booked slots check — nothing booked
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
else:
|
||||
# The actual INSERT
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[insert_result]))
|
||||
else:
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
|
||||
sb.table.side_effect = table_side
|
||||
return sb
|
||||
|
||||
def test_creates_appointment_successfully(self, client):
|
||||
sb = self._sb_with_open_slot()
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.post("/api/v1/chatbots/chatbot-1/appointments", json={
|
||||
"customer_name": "Alice",
|
||||
"customer_contact": "alice@example.com",
|
||||
"slot_start": "2099-06-09T09:00:00",
|
||||
"service": "Consultation",
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["customer_name"] == "Alice"
|
||||
assert resp.json()["status"] == "pending"
|
||||
|
||||
def test_missing_required_fields_returns_422(self, client):
|
||||
sb = make_supabase(chatbot_booking=True)
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.post("/api/v1/chatbots/chatbot-1/appointments",
|
||||
json={"customer_name": "Alice"}) # missing contact + slot
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_404_when_chatbot_not_found(self, client):
|
||||
sb = make_supabase()
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "chatbots":
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.post("/api/v1/chatbots/nonexistent/appointments", json={
|
||||
"customer_name": "Alice",
|
||||
"customer_contact": "alice@example.com",
|
||||
"slot_start": "2099-06-09T09:00:00",
|
||||
})
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_409_when_slot_already_taken(self, client):
|
||||
"""Slot is marked as booked, so should return 409 Conflict."""
|
||||
sb = make_supabase(chatbot_booking=True, hours=[SAMPLE_HOURS])
|
||||
original = sb.table.side_effect
|
||||
def patched(name):
|
||||
t = original(name)
|
||||
if name == "appointments":
|
||||
# 09:00 already booked
|
||||
t.execute = MagicMock(return_value=MagicMock(data=[
|
||||
{"slot_start": "2099-06-09T09:00:00",
|
||||
"slot_end": "2099-06-09T10:00:00"}
|
||||
]))
|
||||
return t
|
||||
sb.table.side_effect = patched
|
||||
with patch("app.routers.appointments.get_supabase", return_value=sb):
|
||||
resp = client.post("/api/v1/chatbots/chatbot-1/appointments", json={
|
||||
"customer_name": "Alice",
|
||||
"customer_contact": "alice@example.com",
|
||||
"slot_start": "2099-06-09T09:00:00",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
Reference in New Issue
Block a user