Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added core/tests/__init__.py
Empty file.
132 changes: 132 additions & 0 deletions core/tests/test_health_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from django.test import TestCase
from rest_framework.test import APITestCase
from rest_framework import status
from unittest.mock import patch, MagicMock
from django.conf import settings
import os

from core.views import HealthCheckView


class HealthCheckViewTest(APITestCase):
def setUp(self):
self.url = '/core/health/'
# Ensure no OpenAI key for some tests
if 'OPENAI_API_KEY' in os.environ:
del os.environ['OPENAI_API_KEY']

@patch('core.views.connection.cursor')
@patch('core.views.Redis.from_url')
@patch('core.views.OpenAI')
def test_health_check_all_healthy(self, mock_openai, mock_redis, mock_cursor):
# Mock DB success
mock_cursor_instance = MagicMock()
mock_cursor.return_value.__enter__.return_value = mock_cursor_instance
mock_cursor_instance.execute.return_value = None

# Mock Redis success
mock_redis_instance = MagicMock()
mock_redis_instance.ping.return_value = True
mock_redis.return_value = mock_redis_instance

# Mock OpenAI success (key set)
os.environ['OPENAI_API_KEY'] = 'test-key'
mock_client = MagicMock()
mock_client.models.list.return_value = MagicMock()
mock_openai.return_value = mock_client

response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(data['overall'], 'healthy')
self.assertEqual(data['services']['db'], 'healthy')
self.assertEqual(data['services']['redis'], 'healthy')
self.assertEqual(data['services']['openai'], 'healthy')

@patch('core.views.connection.cursor')
@patch('core.views.Redis.from_url')
def test_health_check_openai_skipped(self, mock_redis, mock_cursor):
# Mock DB and Redis success
mock_cursor_instance = MagicMock()
mock_cursor.return_value.__enter__.return_value = mock_cursor_instance
mock_cursor_instance.execute.return_value = None

mock_redis_instance = MagicMock()
mock_redis_instance.ping.return_value = True
mock_redis.return_value = mock_redis_instance

# No OpenAI key
if 'OPENAI_API_KEY' in os.environ:
del os.environ['OPENAI_API_KEY']

response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(data['overall'], 'healthy')
self.assertEqual(data['services']['db'], 'healthy')
self.assertEqual(data['services']['redis'], 'healthy')
self.assertEqual(data['services']['openai'], 'skipped')

@patch('core.views.connection.cursor')
@patch('core.views.Redis.from_url')
@patch('core.views.OpenAI')
def test_health_check_openai_unhealthy(self, mock_openai, mock_redis, mock_cursor):
# Mock DB and Redis success
mock_cursor_instance = MagicMock()
mock_cursor.return_value.__enter__.return_value = mock_cursor_instance
mock_cursor_instance.execute.return_value = None

mock_redis_instance = MagicMock()
mock_redis_instance.ping.return_value = True
mock_redis.return_value = mock_redis_instance

# Mock OpenAI failure
os.environ['OPENAI_API_KEY'] = 'test-key'
mock_openai.side_effect = Exception('API error')

response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(data['overall'], 'healthy')
self.assertEqual(data['services']['db'], 'healthy')
self.assertEqual(data['services']['redis'], 'healthy')
self.assertEqual(data['services']['openai'], 'unhealthy')

@patch('core.views.connection.cursor')
@patch('core.views.Redis.from_url')
def test_health_check_db_unhealthy(self, mock_redis, mock_cursor):
# Mock DB failure
mock_cursor.side_effect = Exception('DB error')

# Mock Redis success
mock_redis_instance = MagicMock()
mock_redis_instance.ping.return_value = True
mock_redis.return_value = mock_redis_instance

response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
data = response.json()
self.assertEqual(data['overall'], 'unhealthy')
self.assertEqual(data['services']['db'], 'unhealthy')

@patch('core.views.connection.cursor')
@patch('core.views.Redis.from_url')
def test_health_check_redis_unhealthy(self, mock_redis, mock_cursor):
# Mock DB success
mock_cursor_instance = MagicMock()
mock_cursor.return_value.__enter__.return_value = mock_cursor_instance
mock_cursor_instance.execute.return_value = None

# Mock Redis failure
mock_redis.side_effect = Exception('Redis error')

response = self.client.get(self.url)

self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
data = response.json()
self.assertEqual(data['overall'], 'unhealthy')
self.assertEqual(data['services']['redis'], 'unhealthy')
3 changes: 2 additions & 1 deletion core/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from core.views import CreateCheckoutSessionView, StripeWebhookView, PlanListView, CreateBillingPortalSessionView, VerifyCheckoutSessionView
from core.views import CreateCheckoutSessionView, StripeWebhookView, PlanListView, CreateBillingPortalSessionView, VerifyCheckoutSessionView, HealthCheckView

router = DefaultRouter()

urlpatterns = [
path('health/', HealthCheckView.as_view(), name='health_check'),
path('payments/create-checkout-session/', CreateCheckoutSessionView.as_view(), name='create_checkout_session'),
path('payments/webhook/stripe/', StripeWebhookView.as_view(), name='stripe_webhook'),
path('payments/verify-session/', VerifyCheckoutSessionView.as_view(), name='verify_checkout_session'),
Expand Down
111 changes: 110 additions & 1 deletion core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
from django.core.cache import cache
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from django.db import connection
from redis import Redis
from openai import OpenAI
import os

import stripe
import logging
Expand Down Expand Up @@ -377,4 +381,109 @@ def get(self, request):

except Exception as e:
logger.error(f"❌ Error verifying Stripe session {session_id}: {e}")
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@extend_schema(
responses={
200: {
'type': 'object',
'properties': {
'overall': {'type': 'string'},
'services': {
'type': 'object',
'properties': {
'db': {'type': 'string'},
'redis': {'type': 'string'},
'openai': {'type': 'string'},
}
}
}
},
503: {
'type': 'object',
'properties': {
'overall': {'type': 'string'},
'services': {
'type': 'object',
'properties': {
'db': {'type': 'string'},
'redis': {'type': 'string'},
'openai': {'type': 'string'},
}
}
}
},
},
examples=[
OpenApiExample(
name="Healthy services",
value={
"overall": "healthy",
"services": {
"db": "healthy",
"redis": "healthy",
"openai": "healthy"
}
},
response_only=True
),
OpenApiExample(
name="OpenAI skipped",
value={
"overall": "healthy",
"services": {
"db": "healthy",
"redis": "healthy",
"openai": "skipped"
}
},
response_only=True
)
]
)
class HealthCheckView(APIView):
permission_classes = [AllowAny]

def get(self, request):
services = {}

# Check DB
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
services['db'] = 'healthy'
except Exception as e:
logger.error(f"DB health check failed: {e}")
services['db'] = 'unhealthy'

# Check Redis
try:
redis_client = Redis.from_url(settings.CACHE_URL)
redis_client.ping()
services['redis'] = 'healthy'
except Exception as e:
logger.error(f"Redis health check failed: {e}")
services['redis'] = 'unhealthy'

# Check OpenAI (optional)
api_key = os.getenv("OPENAI_API_KEY")
if api_key:
try:
client = OpenAI(api_key=api_key)
client.models.list() # Lightweight check
services['openai'] = 'healthy'
except Exception as e:
logger.error(f"OpenAI health check failed: {e}")
services['openai'] = 'unhealthy'
else:
services['openai'] = 'skipped'

# Determine overall status
overall = 'healthy' if services['db'] == 'healthy' and services['redis'] == 'healthy' else 'unhealthy'
http_status = status.HTTP_200_OK if overall == 'healthy' else status.HTTP_503_SERVICE_UNAVAILABLE

return Response({
'overall': overall,
'services': services
}, status=http_status)
Loading