"""Flask test client / real-DB integration tests for the manage-booking page (#18), per #14's testing decision: assert on HTTP response + resulting DB state. """ from datetime import date, time, timedelta import pytest import booking_db as bdb from app import app as flask_app from booking_api import _mint_manage_token CLIENT_A = "C-TEST-MANAGE-A" @pytest.fixture def client(): flask_app.config["TESTING"] = True return flask_app.test_client() def _next_monday(after): d = after + timedelta(days=1) while d.weekday() != 0: d += timedelta(days=1) return d def _setup_resource_and_service(client_id=CLIENT_A): with bdb.db.connect() as conn, conn.cursor() as cur: cur.execute( "INSERT INTO clients (client_id, timezone, auto_confirm) VALUES (%s, %s, %s) " "ON CONFLICT (client_id) DO UPDATE SET timezone = EXCLUDED.timezone, " "auto_confirm = EXCLUDED.auto_confirm", (client_id, "Europe/Berlin", True)) conn.commit() resource = bdb.create_resource(client_id, "Chair 1", min_notice_minutes=0, max_advance_days=365) bdb.set_resource_hours(client_id, resource["resource_id"], 0, time(9, 0), time(17, 0)) service = bdb.create_service(client_id, "Haircut", 60, price=25) return resource, service def _create_booking(client, resource, service, slot): return client.post("/api/booking", json={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "service_id": service["service_id"], "start_time": slot, "customer_name": "Dana", "customer_contact": "dana@example.com"}).get_json() def test_manage_page_shows_invalid_message_for_garbage_token(client): resp = client.get("/manage/not-a-real-token") assert resp.status_code == 400 body = resp.get_data(as_text=True) assert "ungültig" in body.lower() or "abgelaufen" in body.lower() def test_manage_page_shows_invalid_message_for_unknown_booking(client): token = _mint_manage_token(CLIENT_A, "BK-does-not-exist") resp = client.get(f"/manage/{token}") assert resp.status_code == 404 def test_manage_page_shows_used_message_for_cancelled_booking(client): resource, service = _setup_resource_and_service() day = _next_monday(date.today()) slot = client.get("/api/booking/slots", query_string={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "service_id": service["service_id"], "date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"][0] created = _create_booking(client, resource, service, slot) client.post("/api/booking/cancel", json={"token": created["token"]}) resp = client.get(f"/manage/{created['token']}") assert resp.status_code == 200 body = resp.get_data(as_text=True) assert "storniert" in body.lower() def test_manage_page_renders_active_booking_with_manage_ui(client): resource, service = _setup_resource_and_service() day = _next_monday(date.today()) slot = client.get("/api/booking/slots", query_string={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "service_id": service["service_id"], "date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"][0] created = _create_booking(client, resource, service, slot) resp = client.get(f"/manage/{created['token']}") assert resp.status_code == 200 body = resp.get_data(as_text=True) assert "Haircut" in body assert resource["resource_id"] in body assert created["token"] in body def test_manage_page_cancel_flow_reaches_cancel_api(client): resource, service = _setup_resource_and_service() day = _next_monday(date.today()) slot = client.get("/api/booking/slots", query_string={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "service_id": service["service_id"], "date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"][0] created = _create_booking(client, resource, service, slot) resp = client.post("/api/booking/cancel", json={"token": created["token"]}) assert resp.status_code == 200 assert bdb.get_booking(CLIENT_A, created["booking_id"])["status"] == "cancelled" followup = client.get(f"/manage/{created['token']}") assert "storniert" in followup.get_data(as_text=True).lower() def test_manage_page_reschedule_slots_browsable_by_duration(client): """The manage page's reschedule picker uses duration_minutes (not service_id, which bookings don't store) against /api/booking/slots.""" resource, service = _setup_resource_and_service() day = _next_monday(date.today()) slot = client.get("/api/booking/slots", query_string={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "service_id": service["service_id"], "date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"][0] created = _create_booking(client, resource, service, slot) resp = client.get("/api/booking/slots", query_string={ "client_id": CLIENT_A, "resource_id": resource["resource_id"], "duration_minutes": "60", "exclude_booking_id": created["booking_id"], "date_from": day.isoformat(), "date_to": day.isoformat()}) assert resp.status_code == 200 slots = resp.get_json()["slots"] # The booking's own slot is excluded from the busy check, so it's # available again for a same-slot reschedule (a no-op success). assert slot in slots reschedule_resp = client.post("/api/booking/reschedule", json={ "token": created["token"], "start_time": slots[1]}) assert reschedule_resp.status_code == 200