Fix 2 broken test assertions, add 22 new tests for register flow, booking email resilience, settings, and customers

This commit is contained in:
belviskhoremk
2026-05-21 00:12:28 +00:00
parent cccb6ded1d
commit 31a33593e1
6 changed files with 335 additions and 11 deletions

View File

@@ -0,0 +1,128 @@
"""
Tests for admin settings and admin customers endpoints.
"""
from unittest.mock import AsyncMock
from tests.conftest import USER_ID
SAMPLE_SETTING = {
"key": "default_booking_price",
"value": 0,
"updated_at": "2026-01-01T00:00:00",
}
SAMPLE_CUSTOMER = {
"id": USER_ID,
"email": "client@test.com",
"full_name": "Marie Dupont",
"phone": "+49123456789",
"is_blocked": False,
"created_at": "2026-01-01T00:00:00",
"orders_count": 2,
"bookings_count": 1,
"total_spent": 378.0,
}
# ── Admin settings ────────────────────────────────────────────────────────────
async def test_get_settings_returns_list(admin_client, mock_db):
mock_db.fetch = AsyncMock(return_value=[SAMPLE_SETTING])
r = await admin_client.get("/api/v1/admin/settings")
assert r.status_code == 200
body = r.json()
assert body["success"] is True
assert isinstance(body["data"], list)
assert body["data"][0]["key"] == "default_booking_price"
async def test_get_settings_empty_table(admin_client, mock_db):
"""Frontend must handle an empty settings list gracefully."""
mock_db.fetch = AsyncMock(return_value=[])
r = await admin_client.get("/api/v1/admin/settings")
assert r.status_code == 200
assert r.json()["data"] == []
async def test_update_setting_upsert(admin_client, mock_db):
saved = {**SAMPLE_SETTING, "value": 50, "updated_at": "2026-05-21T10:00:00"}
mock_db.fetchrow = AsyncMock(return_value=saved)
r = await admin_client.put("/api/v1/admin/settings/default_booking_price", json={"value": 50})
assert r.status_code == 200
body = r.json()
assert body["success"] is True
assert body["data"]["key"] == "default_booking_price"
async def test_update_setting_zero_value(admin_client, mock_db):
"""0 is a valid value (= free appointments)."""
saved = {**SAMPLE_SETTING, "value": 0}
mock_db.fetchrow = AsyncMock(return_value=saved)
r = await admin_client.put("/api/v1/admin/settings/default_booking_price", json={"value": 0})
assert r.status_code == 200
async def test_settings_requires_admin(auth_client):
r = await auth_client.get("/api/v1/admin/settings")
assert r.status_code == 403
async def test_settings_requires_auth(anon_client):
r = await anon_client.get("/api/v1/admin/settings")
assert r.status_code == 401
# ── Admin customers ───────────────────────────────────────────────────────────
async def test_list_customers(admin_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=1)
mock_db.fetch = AsyncMock(return_value=[SAMPLE_CUSTOMER])
r = await admin_client.get("/api/v1/admin/customers")
assert r.status_code == 200
body = r.json()
assert body["success"] is True
data = body["data"]
assert len(data) == 1
assert data[0]["full_name"] == "Marie Dupont"
assert data[0]["orders_count"] == 2
async def test_list_customers_empty(admin_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=0)
mock_db.fetch = AsyncMock(return_value=[])
r = await admin_client.get("/api/v1/admin/customers")
assert r.status_code == 200
assert r.json()["data"] == []
async def test_list_customers_search(admin_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=1)
mock_db.fetch = AsyncMock(return_value=[SAMPLE_CUSTOMER])
r = await admin_client.get("/api/v1/admin/customers?search=marie")
assert r.status_code == 200
async def test_block_customer(admin_client, mock_db):
blocked = {**SAMPLE_CUSTOMER, "is_blocked": True}
mock_db.fetchrow = AsyncMock(return_value=blocked)
r = await admin_client.patch(f"/api/v1/admin/customers/{USER_ID}", json={"is_blocked": True})
assert r.status_code == 200
assert r.json()["data"]["is_blocked"] is True
async def test_unblock_customer(admin_client, mock_db):
unblocked = {**SAMPLE_CUSTOMER, "is_blocked": False}
mock_db.fetchrow = AsyncMock(return_value=unblocked)
r = await admin_client.patch(f"/api/v1/admin/customers/{USER_ID}", json={"is_blocked": False})
assert r.status_code == 200
assert r.json()["data"]["is_blocked"] is False
async def test_customers_requires_admin(auth_client):
r = await auth_client.get("/api/v1/admin/customers")
assert r.status_code == 403
async def test_customers_requires_auth(anon_client):
r = await anon_client.get("/api/v1/admin/customers")
assert r.status_code == 401

View File

@@ -43,8 +43,11 @@ async def test_register_with_name_field(anon_client, mock_db):
"""Frontend sends `name`, not `full_name` — both must be accepted.""" """Frontend sends `name`, not `full_name` — both must be accepted."""
mock_db.execute = AsyncMock(return_value="INSERT 1") mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.auth_service._client") as mock_client: with patch("app.services.auth_service._admin_client") as mock_admin, \
mock_client.return_value.auth.sign_up.return_value = _supabase_session() patch("app.services.auth_service._client") as mock_client:
user = MagicMock(); user.id = "11111111-1111-1111-1111-111111111111"
mock_admin.return_value.auth.admin.create_user.return_value = MagicMock(user=user)
mock_client.return_value.auth.sign_in_with_password.return_value = _supabase_session()
r = await anon_client.post("/api/v1/auth/register", json={ r = await anon_client.post("/api/v1/auth/register", json={
"email": "new@test.com", "email": "new@test.com",
"password": "password123", "password": "password123",
@@ -59,8 +62,11 @@ async def test_register_with_full_name_field(anon_client, mock_db):
"""full_name field also accepted.""" """full_name field also accepted."""
mock_db.execute = AsyncMock(return_value="INSERT 1") mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.auth_service._client") as mock_client: with patch("app.services.auth_service._admin_client") as mock_admin, \
mock_client.return_value.auth.sign_up.return_value = _supabase_session() patch("app.services.auth_service._client") as mock_client:
user = MagicMock(); user.id = "11111111-1111-1111-1111-111111111111"
mock_admin.return_value.auth.admin.create_user.return_value = MagicMock(user=user)
mock_client.return_value.auth.sign_in_with_password.return_value = _supabase_session()
r = await anon_client.post("/api/v1/auth/register", json={ r = await anon_client.post("/api/v1/auth/register", json={
"email": "new2@test.com", "email": "new2@test.com",
"password": "password123", "password": "password123",

View File

@@ -0,0 +1,77 @@
"""
Tests that booking status updates succeed even when the email service fails.
This covers the fix: email send is wrapped in try/except so SMTP failure
does not block the booking status change.
"""
from unittest.mock import AsyncMock, patch
from tests.conftest import SAMPLE_BOOKING, BOOKING_ID, ADMIN_ID
async def test_confirm_booking_succeeds_when_email_fails(admin_client, mock_db):
"""Admin confirming a booking must succeed even if the email service raises."""
mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_BOOKING, # UPDATE bookings RETURNING *
{"email": "client@test.com"}, # SELECT email FROM profiles
{"date": "2026-06-01", "start_time": "10:00:00"}, # SELECT from time_slots
SAMPLE_BOOKING, # get_booking at the end
])
mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.email_service.send_booking_confirmed",
new_callable=AsyncMock, side_effect=Exception("SMTP unavailable")):
r = await admin_client.patch(
f"/api/v1/admin/bookings/{BOOKING_ID}",
json={"status": "confirmed"},
)
# Must succeed despite the email failure
assert r.status_code == 200
assert r.json()["success"] is True
async def test_cancel_booking_succeeds_when_email_fails(admin_client, mock_db):
"""Same resilience for cancellation emails."""
pending = {**SAMPLE_BOOKING, "status": "pending"}
mock_db.fetchrow = AsyncMock(side_effect=[
pending,
{"email": "client@test.com"},
{"date": "2026-06-01", "start_time": "10:00:00"},
pending,
])
mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.email_service.send_booking_cancelled",
new_callable=AsyncMock, side_effect=Exception("SMTP unavailable")):
r = await admin_client.patch(
f"/api/v1/admin/bookings/{BOOKING_ID}",
json={"status": "cancelled"},
)
assert r.status_code == 200
assert r.json()["success"] is True
async def test_confirm_booking_with_no_email_address(admin_client, mock_db):
"""Booking with no guest_email and no user_id must still update successfully."""
no_email_booking = {**SAMPLE_BOOKING, "guest_email": None, "user_id": None}
mock_db.fetchrow = AsyncMock(side_effect=[
no_email_booking, # UPDATE RETURNING *
no_email_booking, # get_booking at the end
])
mock_db.execute = AsyncMock(return_value="INSERT 1")
r = await admin_client.patch(
f"/api/v1/admin/bookings/{BOOKING_ID}",
json={"status": "confirmed"},
)
assert r.status_code == 200
async def test_confirm_booking_not_found(admin_client, mock_db):
mock_db.fetchrow = AsyncMock(return_value=None)
r = await admin_client.patch(
f"/api/v1/admin/bookings/00000000-0000-0000-0000-000000000000",
json={"status": "confirmed"},
)
assert r.status_code == 404
assert r.json()["error"]["code"] == "BOOKING_NOT_FOUND"

View File

@@ -25,10 +25,9 @@ async def test_get_slots_missing_date_param(anon_client):
async def test_guest_booking_success(anon_client, mock_db): async def test_guest_booking_success(anon_client, mock_db):
mock_db.fetchrow = AsyncMock(side_effect=[ mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_SLOT, # slot availability check SAMPLE_SLOT, # slot availability check
SAMPLE_BOOKING, # INSERT booking SAMPLE_BOOKING, # INSERT booking RETURNING *
]) ])
mock_db.fetchval = AsyncMock(return_value=50.0) # default booking price
r = await anon_client.post("/api/v1/bookings", json={ r = await anon_client.post("/api/v1/bookings", json={
"slot_id": SLOT_ID, "slot_id": SLOT_ID,
@@ -40,7 +39,6 @@ async def test_guest_booking_success(anon_client, mock_db):
assert r.status_code == 201 assert r.status_code == 201
body = r.json() body = r.json()
assert body["success"] is True assert body["success"] is True
assert "client_secret" in body["data"]
assert "booking_id" in body["data"] assert "booking_id" in body["data"]

View File

@@ -25,8 +25,8 @@ SAMPLE_ORDER_ROW = {
async def test_create_order_success(auth_client, mock_db): async def test_create_order_success(auth_client, mock_db):
mock_db.fetchrow = AsyncMock(side_effect=[ mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_PRODUCT_ROW, # product lookup SAMPLE_PRODUCT_ROW, # product lookup
{"id": ORDER_ID, **SAMPLE_ORDER_ROW}, # INSERT order {"id": ORDER_ID, **SAMPLE_ORDER_ROW}, # INSERT order RETURNING *
]) ])
mock_db.execute = AsyncMock(return_value="UPDATE 1") mock_db.execute = AsyncMock(return_value="UPDATE 1")
@@ -36,7 +36,6 @@ async def test_create_order_success(auth_client, mock_db):
assert r.status_code == 201 assert r.status_code == 201
body = r.json() body = r.json()
assert body["success"] is True assert body["success"] is True
assert "client_secret" in body["data"]
assert "order_id" in body["data"] assert "order_id" in body["data"]
assert body["data"]["amount"] == 189.0 assert body["data"]["amount"] == 189.0

116
tests/test_register_flow.py Normal file
View File

@@ -0,0 +1,116 @@
"""
Tests for the updated registration flow:
- Uses admin API (create_user with email_confirm=True) instead of sign_up
- Immediately signs in after creation so a session is always returned
- full_name / name field resolved correctly
"""
from unittest.mock import AsyncMock, MagicMock, patch
def _admin_create_result(user_id="11111111-1111-1111-1111-111111111111"):
user = MagicMock()
user.id = user_id
result = MagicMock()
result.user = user
return result
def _sign_in_result(access_token="tok_access", refresh_token="tok_refresh"):
session = MagicMock()
session.access_token = access_token
session.refresh_token = refresh_token
session.expires_in = 3600
result = MagicMock()
result.session = session
return result
async def test_register_returns_tokens_immediately(anon_client, mock_db):
"""Registration must always return access_token — no email confirmation delay."""
mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.auth_service._admin_client") as mock_admin, \
patch("app.services.auth_service._client") as mock_client:
mock_admin.return_value.auth.admin.create_user.return_value = _admin_create_result()
mock_client.return_value.auth.sign_in_with_password.return_value = _sign_in_result()
r = await anon_client.post("/api/v1/auth/register", json={
"email": "new@test.com",
"password": "password123",
"name": "Marie Dupont",
})
assert r.status_code == 201
body = r.json()
assert body["success"] is True
assert "access_token" in body["data"]
assert "refresh_token" in body["data"]
assert body["data"]["token_type"] == "bearer"
async def test_register_stores_full_name_from_name_field(anon_client, mock_db):
"""The `name` field sent by the frontend must be stored in the profile."""
executed_args = []
async def capture_execute(query, *args):
executed_args.append(args)
return "INSERT 1"
mock_db.execute = capture_execute
with patch("app.services.auth_service._admin_client") as mock_admin, \
patch("app.services.auth_service._client") as mock_client:
mock_admin.return_value.auth.admin.create_user.return_value = _admin_create_result()
mock_client.return_value.auth.sign_in_with_password.return_value = _sign_in_result()
await anon_client.post("/api/v1/auth/register", json={
"email": "new@test.com",
"password": "password123",
"name": "Marie Dupont",
})
# The INSERT INTO profiles call should include the name
assert any("Marie Dupont" in str(args) for args in executed_args)
async def test_register_stores_full_name_field(anon_client, mock_db):
"""The `full_name` field is also accepted as an alias for `name`."""
mock_db.execute = AsyncMock(return_value="INSERT 1")
with patch("app.services.auth_service._admin_client") as mock_admin, \
patch("app.services.auth_service._client") as mock_client:
mock_admin.return_value.auth.admin.create_user.return_value = _admin_create_result()
mock_client.return_value.auth.sign_in_with_password.return_value = _sign_in_result()
r = await anon_client.post("/api/v1/auth/register", json={
"email": "new2@test.com",
"password": "password123",
"full_name": "Sophie Klein",
})
assert r.status_code == 201
assert "access_token" in r.json()["data"]
async def test_register_supabase_error_returns_400(anon_client, mock_db):
"""If Supabase admin API fails, a 400 with REGISTRATION_FAILED is returned."""
with patch("app.services.auth_service._admin_client") as mock_admin:
mock_admin.return_value.auth.admin.create_user.side_effect = Exception("Email already registered")
r = await anon_client.post("/api/v1/auth/register", json={
"email": "existing@test.com",
"password": "password123",
"name": "Test",
})
assert r.status_code == 400
body = r.json()
assert body["success"] is False
assert body["error"]["code"] == "REGISTRATION_FAILED"
async def test_register_password_validation(anon_client):
r = await anon_client.post("/api/v1/auth/register", json={
"email": "x@test.com",
"password": "short",
"name": "X",
})
assert r.status_code == 422