mirror of
http://88.130.71.182:3000/BlitTech/badoHair_be.git
synced 2026-06-12 23:23:22 +00:00
Fix 2 broken test assertions, add 22 new tests for register flow, booking email resilience, settings, and customers
This commit is contained in:
128
tests/test_admin_settings_customers.py
Normal file
128
tests/test_admin_settings_customers.py
Normal 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
|
||||
@@ -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."""
|
||||
mock_db.execute = AsyncMock(return_value="INSERT 1")
|
||||
|
||||
with patch("app.services.auth_service._client") as mock_client:
|
||||
mock_client.return_value.auth.sign_up.return_value = _supabase_session()
|
||||
with patch("app.services.auth_service._admin_client") as mock_admin, \
|
||||
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={
|
||||
"email": "new@test.com",
|
||||
"password": "password123",
|
||||
@@ -59,8 +62,11 @@ async def test_register_with_full_name_field(anon_client, mock_db):
|
||||
"""full_name field also accepted."""
|
||||
mock_db.execute = AsyncMock(return_value="INSERT 1")
|
||||
|
||||
with patch("app.services.auth_service._client") as mock_client:
|
||||
mock_client.return_value.auth.sign_up.return_value = _supabase_session()
|
||||
with patch("app.services.auth_service._admin_client") as mock_admin, \
|
||||
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={
|
||||
"email": "new2@test.com",
|
||||
"password": "password123",
|
||||
|
||||
77
tests/test_booking_email_resilience.py
Normal file
77
tests/test_booking_email_resilience.py
Normal 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"
|
||||
@@ -25,10 +25,9 @@ async def test_get_slots_missing_date_param(anon_client):
|
||||
|
||||
async def test_guest_booking_success(anon_client, mock_db):
|
||||
mock_db.fetchrow = AsyncMock(side_effect=[
|
||||
SAMPLE_SLOT, # slot availability check
|
||||
SAMPLE_BOOKING, # INSERT booking
|
||||
SAMPLE_SLOT, # slot availability check
|
||||
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={
|
||||
"slot_id": SLOT_ID,
|
||||
@@ -40,7 +39,6 @@ async def test_guest_booking_success(anon_client, mock_db):
|
||||
assert r.status_code == 201
|
||||
body = r.json()
|
||||
assert body["success"] is True
|
||||
assert "client_secret" in body["data"]
|
||||
assert "booking_id" in body["data"]
|
||||
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ SAMPLE_ORDER_ROW = {
|
||||
|
||||
async def test_create_order_success(auth_client, mock_db):
|
||||
mock_db.fetchrow = AsyncMock(side_effect=[
|
||||
SAMPLE_PRODUCT_ROW, # product lookup
|
||||
{"id": ORDER_ID, **SAMPLE_ORDER_ROW}, # INSERT order
|
||||
SAMPLE_PRODUCT_ROW, # product lookup
|
||||
{"id": ORDER_ID, **SAMPLE_ORDER_ROW}, # INSERT order RETURNING *
|
||||
])
|
||||
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
|
||||
body = r.json()
|
||||
assert body["success"] is True
|
||||
assert "client_secret" in body["data"]
|
||||
assert "order_id" in body["data"]
|
||||
assert body["data"]["amount"] == 189.0
|
||||
|
||||
|
||||
116
tests/test_register_flow.py
Normal file
116
tests/test_register_flow.py
Normal 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
|
||||
Reference in New Issue
Block a user