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:
belviskhoremk
2026-04-03 09:11:58 +00:00
parent 9dccc83293
commit 92d4c2fc5e
51 changed files with 7076 additions and 515 deletions

94
tests/test_admin.py Normal file
View File

@@ -0,0 +1,94 @@
"""Tests for admin endpoints — access control and basic structure."""
import pytest
from unittest.mock import MagicMock, patch
class TestAdminAccessControl:
"""Admin endpoints must return 401 without auth and 403 for non-admin users."""
ADMIN_ENDPOINTS = [
("GET", "/api/v1/admin/stats"),
("GET", "/api/v1/admin/users"),
("GET", "/api/v1/admin/chatbots"),
("GET", "/api/v1/admin/conversations"),
("GET", "/api/v1/admin/system/health"),
]
def test_admin_endpoints_require_auth(self, client):
for method, path in self.ADMIN_ENDPOINTS:
resp = client.request(method, path)
assert resp.status_code == 401, f"{method} {path} should require auth, got {resp.status_code}"
def test_non_admin_user_gets_403(self, client):
"""Authenticated user without is_admin flag should get 403."""
user = MagicMock()
user.id = "normal-user-id"
user.email = "user@example.com"
with patch("app.dependencies.get_supabase") as mock_sb:
sb = MagicMock()
# get_current_user: no suspension
sb.table.return_value.select.return_value.eq.return_value.execute.return_value = MagicMock(
data=[{"suspended_at": None, "is_admin": False}]
)
mock_sb.return_value = sb
with patch("app.dependencies.security") as mock_sec:
creds = MagicMock()
creds.credentials = "valid-token"
mock_sec.return_value = creds
with patch("app.database.get_supabase") as mock_db_sb:
db_sb = MagicMock()
db_sb.auth.get_user.return_value = MagicMock(user=user)
# Profile: not admin, not suspended
db_sb.table.return_value.select.return_value.eq.return_value.execute.return_value = MagicMock(
data=[{"is_admin": False, "suspended_at": None}]
)
mock_db_sb.return_value = db_sb
resp = client.get(
"/api/v1/admin/stats",
headers={"Authorization": "Bearer valid-token"},
)
assert resp.status_code in (401, 403)
class TestAdminModels:
def test_admin_stats_response_shape(self):
from app.models import AdminStatsResponse
stats = AdminStatsResponse(
total_users=10,
total_chatbots=5,
total_published_chatbots=3,
total_conversations=100,
total_messages=500,
active_subscriptions={"free": 8, "starter": 2},
)
assert stats.total_users == 10
assert stats.active_subscriptions["free"] == 8
def test_admin_user_list_item_defaults(self):
from app.models import AdminUserListItem
item = AdminUserListItem(id="id", email="test@example.com")
assert item.plan == "free"
assert item.is_admin == False
assert item.is_suspended == False
def test_admin_system_health_shape(self):
from app.models import AdminSystemHealth
from datetime import datetime
health = AdminSystemHealth(
db="healthy",
qdrant="healthy",
llm_providers={"openai": True, "anthropic": False},
timestamp=datetime.utcnow(),
)
assert health.db == "healthy"
assert health.llm_providers["openai"] == True
def test_change_plan_request_validates(self):
from app.models import AdminChangePlanRequest
req = AdminChangePlanRequest(plan="business", reason="Testing")
assert req.plan == "business"