Skip to content
Draft
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
93 changes: 93 additions & 0 deletions readthedocs/core/tests/test_operations_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Tests for operations views."""

import django_dynamic_fixture as fixture
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse


class OperationsViewTest(TestCase):
"""Test operations views for Sentry and New Relic."""

def setUp(self):
"""Set up test users."""
self.staff_user = fixture.get(User, username="staff", is_staff=True)
self.normal_user = fixture.get(User, username="normal", is_staff=False)

def test_sentry_view_requires_authentication(self):
"""Test that anonymous users cannot access Sentry operations view."""
response = self.client.get(reverse("operations_sentry"))
# Should return 403 Forbidden for anonymous users
assert response.status_code == 403
data = response.json()
assert "error" in data

def test_sentry_view_requires_staff(self):
"""Test that normal users cannot access Sentry operations view."""
self.client.force_login(self.normal_user)
response = self.client.get(reverse("operations_sentry"))
# Should return 403 Forbidden
assert response.status_code == 403

def test_sentry_view_staff_access(self):
"""Test that staff users can access Sentry operations view."""
self.client.force_login(self.staff_user)
response = self.client.get(reverse("operations_sentry"))
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["service"] == "sentry"
assert "sentry" in data["message"]

def test_newrelic_view_requires_authentication(self):
"""Test that anonymous users cannot access New Relic operations view."""
response = self.client.get(reverse("operations_newrelic"))
# Should return 403 Forbidden for anonymous users
assert response.status_code == 403
data = response.json()
assert "error" in data

def test_newrelic_view_requires_staff(self):
"""Test that normal users cannot access New Relic operations view."""
self.client.force_login(self.normal_user)
response = self.client.get(reverse("operations_newrelic"))
# Should return 403 Forbidden
assert response.status_code == 403

def test_newrelic_view_staff_access(self):
"""Test that staff users can access New Relic operations view."""
self.client.force_login(self.staff_user)
response = self.client.get(reverse("operations_newrelic"))
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["service"] == "newrelic"
assert "newrelic" in data["message"]

def test_sentry_view_generates_log_messages(self):
"""Test that Sentry operations view generates log messages."""
self.client.force_login(self.staff_user)
with self.assertLogs("readthedocs.core.views", level="INFO") as logs:
response = self.client.get(reverse("operations_sentry"))
assert response.status_code == 200
# Check that both info and error logs were generated
log_output = "\n".join(logs.output)
assert "sentry logging check" in log_output
assert "sentry error logging check" in log_output
# Check that both INFO and ERROR levels were used
assert any("INFO" in log for log in logs.output)
assert any("ERROR" in log for log in logs.output)

def test_newrelic_view_generates_log_messages(self):
"""Test that New Relic operations view generates log messages."""
self.client.force_login(self.staff_user)
with self.assertLogs("readthedocs.core.views", level="INFO") as logs:
response = self.client.get(reverse("operations_newrelic"))
assert response.status_code == 200
# Check that both info and error logs were generated
log_output = "\n".join(logs.output)
assert "newrelic logging check" in log_output
assert "newrelic error logging check" in log_output
# Check that both INFO and ERROR levels were used
assert any("INFO" in log for log in logs.output)
assert any("ERROR" in log for log in logs.output)
65 changes: 65 additions & 0 deletions readthedocs/core/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import structlog
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import Http404
from django.http import JsonResponse
from django.shortcuts import redirect
Expand Down Expand Up @@ -168,3 +169,67 @@ def do_not_track(request):
},
content_type="application/tracking-status+json",
)


class OperationsLogView(PrivateViewMixin, UserPassesTestMixin, View):
"""
Operations view for testing logging to external services.

This view is used to verify that logging to services like Sentry and New Relic
is working correctly after deployments or upgrades. It requires staff access
and generates both info and error level log messages.
"""

service_name = None
raise_exception = True

def test_func(self):
"""Only allow staff users to access this view."""
return self.request.user.is_staff

def handle_no_permission(self):
"""Return a JSON 403 response instead of rendering a template."""
return JsonResponse(
{"error": "Access denied. Staff privileges required."},
status=403,
)

def get(self, request, *args, **kwargs):
"""Generate log messages for the configured service."""
if not self.service_name:
return JsonResponse(
{"error": "Service name not configured"},
status=500,
)

log.info(
f"Operations test: {self.service_name} logging check",
service=self.service_name,
user=request.user.username,
)
log.error(
f"Operations test error: {self.service_name} error logging check",
service=self.service_name,
user=request.user.username,
)

return JsonResponse(
{
"status": "ok",
"service": self.service_name,
"message": f"Log messages sent to {self.service_name}",
},
status=200,
)


class SentryOperationsView(OperationsLogView):
"""Operations view for testing Sentry logging."""

service_name = "sentry"


class NewRelicOperationsView(OperationsLogView):
"""Operations view for testing New Relic logging."""

service_name = "newrelic"
8 changes: 8 additions & 0 deletions readthedocs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from readthedocs.core.views import ErrorView
from readthedocs.core.views import HomepageView
from readthedocs.core.views import NewRelicOperationsView
from readthedocs.core.views import SentryOperationsView
from readthedocs.core.views import SupportView
from readthedocs.core.views import WelcomeView
from readthedocs.core.views import do_not_track
Expand Down Expand Up @@ -144,6 +146,11 @@
),
]

operations_urls = [
path("operations/sentry/", SentryOperationsView.as_view(), name="operations_sentry"),
path("operations/newrelic/", NewRelicOperationsView.as_view(), name="operations_newrelic"),
]

debug_urls = []
for build_format in ("epub", "htmlzip", "json", "pdf"):
debug_urls += static(
Expand Down Expand Up @@ -189,6 +196,7 @@
if settings.ALLOW_ADMIN:
groups.append(admin_urls)
groups.append(impersonate_urls)
groups.append(operations_urls)

if settings.SHOW_DEBUG_TOOLBAR:
import debug_toolbar
Expand Down
Loading