c3e520aabb
Test backoffice (smb-crm) / test (push) Successful in 1m43s
Public booking API now rejects a 6th active booking from the same customer_contact within 24h (429), stopping one contact from filling every slot on every resource, while owner-entered manual bookings stay unaffected. Add POST /api/contact: client sites can reach their own owner's inbox directly (via their existing login email) for general inquiries, separate from the agency's leads/Telegram pipeline (n8n/lead-intake.json), which stays reserved for actual prospects contacting the agency itself. Paris Barber Shop's contact form and Rückruf widget now point here; the Rückruf floating widget itself has been removed from the site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""Flask test client / real-DB integration tests for the public "contact the
|
|
owner" API (contact_api.py) -- the customer-to-owner path, deliberately
|
|
separate from the agency's leads/Telegram pipeline. See contact_mail.py.
|
|
"""
|
|
import pytest
|
|
|
|
import booking_db as bdb
|
|
from app import app as flask_app
|
|
|
|
CLIENT_A = "C-TEST-CONTACT-A"
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
flask_app.config["TESTING"] = True
|
|
return flask_app.test_client()
|
|
|
|
|
|
def _insert_booking_client(client_id, business_name="Café Test"):
|
|
with bdb.db.connect() as conn, conn.cursor() as cur:
|
|
cur.execute(
|
|
"INSERT INTO clients (client_id, business_name) VALUES (%s, %s) "
|
|
"ON CONFLICT (client_id) DO NOTHING",
|
|
(client_id, business_name))
|
|
conn.commit()
|
|
|
|
|
|
def test_contact_request_emails_the_owner(client, monkeypatch):
|
|
sent = []
|
|
monkeypatch.setattr(
|
|
"contact_mail.mailer.send_email",
|
|
lambda to, subject, html, from_addr=None: sent.append(
|
|
{"to": to, "subject": subject, "html": html}))
|
|
_insert_booking_client(CLIENT_A)
|
|
bdb.create_user(CLIENT_A, "inhaber@contact-test.example", "irrelevant-pw")
|
|
|
|
resp = client.post("/api/contact", json={
|
|
"client_id": CLIENT_A, "name": "Jamie", "contact": "jamie@example.com",
|
|
"service_interest": "Herrenschnitt", "message": "Gibt es heute noch einen Termin?"})
|
|
assert resp.status_code == 201
|
|
assert resp.get_json() == {"sent": True}
|
|
assert resp.headers["Access-Control-Allow-Origin"] == "*"
|
|
|
|
assert len(sent) == 1
|
|
assert sent[0]["to"] == "inhaber@contact-test.example"
|
|
assert "Jamie" in sent[0]["html"]
|
|
assert "jamie@example.com" in sent[0]["html"]
|
|
assert "Herrenschnitt" in sent[0]["html"]
|
|
|
|
|
|
def test_contact_request_falls_back_to_service_field(client, monkeypatch):
|
|
"""The Rückruf widget sends service_interest; the plain contact form
|
|
sends the same concept under the field name "service" -- both work."""
|
|
sent = []
|
|
monkeypatch.setattr(
|
|
"contact_mail.mailer.send_email",
|
|
lambda to, subject, html, from_addr=None: sent.append(html))
|
|
client_id = "C-TEST-CONTACT-SVCFALLBACK"
|
|
_insert_booking_client(client_id)
|
|
bdb.create_user(client_id, "inhaber@svcfallback.example", "irrelevant-pw")
|
|
|
|
resp = client.post("/api/contact", json={
|
|
"client_id": client_id, "name": "Robin", "contact": "robin@example.com",
|
|
"service": "Bartpflege"})
|
|
assert resp.status_code == 201
|
|
assert "Bartpflege" in sent[0]
|
|
|
|
|
|
def test_honeypot_fakes_success_without_sending(client, monkeypatch):
|
|
sent = []
|
|
monkeypatch.setattr(
|
|
"contact_mail.mailer.send_email",
|
|
lambda to, subject, html, from_addr=None: sent.append(to))
|
|
client_id = "C-TEST-CONTACT-HONEYPOT"
|
|
_insert_booking_client(client_id)
|
|
bdb.create_user(client_id, "inhaber@honeypot.example", "irrelevant-pw")
|
|
|
|
resp = client.post("/api/contact", json={
|
|
"client_id": client_id, "name": "Bot", "contact": "bot@example.com",
|
|
"website": "http://spam.example"})
|
|
assert resp.status_code == 201
|
|
assert resp.get_json() == {"sent": True}
|
|
assert sent == []
|
|
|
|
|
|
def test_missing_required_fields_is_rejected(client):
|
|
resp = client.post("/api/contact", json={"client_id": CLIENT_A, "name": "NoContact"})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_unknown_client_is_rejected(client):
|
|
resp = client.post("/api/contact", json={
|
|
"client_id": "C-DOES-NOT-EXIST", "name": "Alex", "contact": "a@example.com"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_client_without_owner_account_is_rejected(client):
|
|
"""A client that exists in the CRM but hasn't been provisioned into the
|
|
booking schema yet (no owner account) -- nothing to notify, so this is a
|
|
404 rather than a silent success the visitor would wrongly trust."""
|
|
client_id = "C-TEST-CONTACT-NOOWNER"
|
|
_insert_booking_client(client_id)
|
|
resp = client.post("/api/contact", json={
|
|
"client_id": client_id, "name": "Alex", "contact": "a@example.com"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_preflight_options_returns_cors_headers(client):
|
|
resp = client.options("/api/contact")
|
|
assert resp.status_code < 300
|
|
assert resp.headers["Access-Control-Allow-Origin"] == "*"
|
|
assert "POST" in resp.headers["Access-Control-Allow-Methods"]
|