mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-15 12:38:03 +00:00
File upload security (iteration 1), security enhancements and OpenTelemetry (OTEL) implementation (sending data disabled by default)
This commit is contained in:
439
ivatar/test_opentelemetry.py
Normal file
439
ivatar/test_opentelemetry.py
Normal file
@@ -0,0 +1,439 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for OpenTelemetry integration in ivatar.
|
||||
|
||||
This module contains comprehensive tests for OpenTelemetry functionality,
|
||||
including configuration, middleware, metrics, and tracing.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.http import HttpResponse
|
||||
|
||||
from ivatar.opentelemetry_config import (
|
||||
OpenTelemetryConfig,
|
||||
is_enabled,
|
||||
)
|
||||
from ivatar.opentelemetry_middleware import (
|
||||
OpenTelemetryMiddleware,
|
||||
trace_avatar_operation,
|
||||
trace_file_upload,
|
||||
trace_authentication,
|
||||
AvatarMetrics,
|
||||
get_avatar_metrics,
|
||||
reset_avatar_metrics,
|
||||
)
|
||||
|
||||
|
||||
class OpenTelemetryConfigTest(TestCase):
|
||||
"""Test OpenTelemetry configuration."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = os.environ.copy()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
os.environ.clear()
|
||||
os.environ.update(self.original_env)
|
||||
|
||||
def test_config_always_enabled(self):
|
||||
"""Test that OpenTelemetry instrumentation is always enabled."""
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertTrue(config.enabled)
|
||||
|
||||
def test_config_enabled_with_env_var(self):
|
||||
"""Test that OpenTelemetry can be enabled with environment variable."""
|
||||
os.environ["OTEL_ENABLED"] = "true"
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertTrue(config.enabled)
|
||||
|
||||
def test_service_name_default(self):
|
||||
"""Test default service name."""
|
||||
# Clear environment variables to test default behavior
|
||||
original_env = os.environ.copy()
|
||||
os.environ.pop("OTEL_SERVICE_NAME", None)
|
||||
|
||||
try:
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertEqual(config.service_name, "ivatar")
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(original_env)
|
||||
|
||||
def test_service_name_custom(self):
|
||||
"""Test custom service name."""
|
||||
os.environ["OTEL_SERVICE_NAME"] = "custom-service"
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertEqual(config.service_name, "custom-service")
|
||||
|
||||
def test_environment_default(self):
|
||||
"""Test default environment."""
|
||||
# Clear environment variables to test default behavior
|
||||
original_env = os.environ.copy()
|
||||
os.environ.pop("OTEL_ENVIRONMENT", None)
|
||||
|
||||
try:
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertEqual(config.environment, "development")
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(original_env)
|
||||
|
||||
def test_environment_custom(self):
|
||||
"""Test custom environment."""
|
||||
os.environ["OTEL_ENVIRONMENT"] = "production"
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertEqual(config.environment, "production")
|
||||
|
||||
def test_resource_creation(self):
|
||||
"""Test resource creation with service information."""
|
||||
os.environ["OTEL_SERVICE_NAME"] = "test-service"
|
||||
os.environ["OTEL_ENVIRONMENT"] = "test"
|
||||
os.environ["IVATAR_VERSION"] = "1.0.0"
|
||||
os.environ["HOSTNAME"] = "test-host"
|
||||
|
||||
config = OpenTelemetryConfig()
|
||||
resource = config.resource
|
||||
|
||||
self.assertEqual(resource.attributes["service.name"], "test-service")
|
||||
self.assertEqual(resource.attributes["service.version"], "1.0.0")
|
||||
self.assertEqual(resource.attributes["deployment.environment"], "test")
|
||||
self.assertEqual(resource.attributes["service.instance.id"], "test-host")
|
||||
|
||||
@patch("ivatar.opentelemetry_config.OTLPSpanExporter")
|
||||
@patch("ivatar.opentelemetry_config.BatchSpanProcessor")
|
||||
@patch("ivatar.opentelemetry_config.trace")
|
||||
def test_setup_tracing_with_otlp(self, mock_trace, mock_processor, mock_exporter):
|
||||
"""Test tracing setup with OTLP endpoint."""
|
||||
os.environ["OTEL_EXPORT_ENABLED"] = "true"
|
||||
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317"
|
||||
|
||||
config = OpenTelemetryConfig()
|
||||
config.setup_tracing()
|
||||
|
||||
mock_exporter.assert_called_once_with(endpoint="http://localhost:4317")
|
||||
mock_processor.assert_called_once()
|
||||
mock_trace.get_tracer_provider().add_span_processor.assert_called_once()
|
||||
|
||||
@patch("ivatar.opentelemetry_config.PrometheusMetricReader")
|
||||
@patch("ivatar.opentelemetry_config.PeriodicExportingMetricReader")
|
||||
@patch("ivatar.opentelemetry_config.OTLPMetricExporter")
|
||||
@patch("ivatar.opentelemetry_config.metrics")
|
||||
def test_setup_metrics_with_prometheus_and_otlp(
|
||||
self,
|
||||
mock_metrics,
|
||||
mock_otlp_exporter,
|
||||
mock_periodic_reader,
|
||||
mock_prometheus_reader,
|
||||
):
|
||||
"""Test metrics setup with Prometheus and OTLP."""
|
||||
os.environ["OTEL_EXPORT_ENABLED"] = "true"
|
||||
os.environ["OTEL_PROMETHEUS_ENDPOINT"] = "0.0.0.0:9464"
|
||||
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317"
|
||||
|
||||
config = OpenTelemetryConfig()
|
||||
config.setup_metrics()
|
||||
|
||||
mock_prometheus_reader.assert_called_once()
|
||||
mock_otlp_exporter.assert_called_once_with(endpoint="http://localhost:4317")
|
||||
mock_periodic_reader.assert_called_once()
|
||||
mock_metrics.set_meter_provider.assert_called_once()
|
||||
|
||||
@patch("ivatar.opentelemetry_config.DjangoInstrumentor")
|
||||
@patch("ivatar.opentelemetry_config.Psycopg2Instrumentor")
|
||||
@patch("ivatar.opentelemetry_config.PyMySQLInstrumentor")
|
||||
@patch("ivatar.opentelemetry_config.RequestsInstrumentor")
|
||||
@patch("ivatar.opentelemetry_config.URLLib3Instrumentor")
|
||||
def test_setup_instrumentation(
|
||||
self,
|
||||
mock_urllib3,
|
||||
mock_requests,
|
||||
mock_pymysql,
|
||||
mock_psycopg2,
|
||||
mock_django,
|
||||
):
|
||||
"""Test instrumentation setup."""
|
||||
os.environ["OTEL_ENABLED"] = "true"
|
||||
|
||||
config = OpenTelemetryConfig()
|
||||
config.setup_instrumentation()
|
||||
|
||||
mock_django().instrument.assert_called_once()
|
||||
mock_psycopg2().instrument.assert_called_once()
|
||||
mock_pymysql().instrument.assert_called_once()
|
||||
mock_requests().instrument.assert_called_once()
|
||||
mock_urllib3().instrument.assert_called_once()
|
||||
|
||||
|
||||
class OpenTelemetryMiddlewareTest(TestCase):
|
||||
"""Test OpenTelemetry middleware."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.factory = RequestFactory()
|
||||
reset_avatar_metrics() # Reset global metrics instance
|
||||
self.middleware = OpenTelemetryMiddleware(lambda r: HttpResponse("test"))
|
||||
|
||||
@patch("ivatar.opentelemetry_middleware.get_tracer")
|
||||
def test_middleware_enabled(self, mock_get_tracer):
|
||||
"""Test middleware when OpenTelemetry is enabled."""
|
||||
mock_tracer = MagicMock()
|
||||
mock_span = MagicMock()
|
||||
mock_tracer.start_span.return_value = mock_span
|
||||
mock_get_tracer.return_value = mock_tracer
|
||||
|
||||
request = self.factory.get("/avatar/test@example.com")
|
||||
response = self.middleware(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(hasattr(request, "_ot_span"))
|
||||
mock_tracer.start_span.assert_called_once()
|
||||
mock_span.set_attributes.assert_called()
|
||||
mock_span.end.assert_called_once()
|
||||
|
||||
@patch("ivatar.opentelemetry_middleware.get_tracer")
|
||||
def test_avatar_request_attributes(self, mock_get_tracer):
|
||||
"""Test that avatar requests get proper attributes."""
|
||||
mock_tracer = MagicMock()
|
||||
mock_span = MagicMock()
|
||||
mock_tracer.start_span.return_value = mock_span
|
||||
mock_get_tracer.return_value = mock_tracer
|
||||
|
||||
request = self.factory.get("/avatar/test@example.com?s=128&d=png")
|
||||
# Reset metrics to ensure we get a fresh instance
|
||||
reset_avatar_metrics()
|
||||
self.middleware.process_request(request)
|
||||
|
||||
# Check that avatar-specific attributes were set
|
||||
calls = mock_span.set_attributes.call_args_list
|
||||
avatar_attrs = any(
|
||||
call[0][0].get("ivatar.request_type") == "avatar" for call in calls
|
||||
)
|
||||
# Also check for individual set_attribute calls
|
||||
set_attribute_calls = mock_span.set_attribute.call_args_list
|
||||
individual_avatar_attrs = any(
|
||||
call[0][0] == "ivatar.request_type" and call[0][1] == "avatar"
|
||||
for call in set_attribute_calls
|
||||
)
|
||||
self.assertTrue(avatar_attrs or individual_avatar_attrs)
|
||||
|
||||
def test_is_avatar_request(self):
|
||||
"""Test avatar request detection."""
|
||||
avatar_request = self.factory.get("/avatar/test@example.com")
|
||||
non_avatar_request = self.factory.get("/stats/")
|
||||
|
||||
self.assertTrue(self.middleware._is_avatar_request(avatar_request))
|
||||
self.assertFalse(self.middleware._is_avatar_request(non_avatar_request))
|
||||
|
||||
def test_get_avatar_size(self):
|
||||
"""Test avatar size extraction."""
|
||||
request = self.factory.get("/avatar/test@example.com?s=256")
|
||||
size = self.middleware._get_avatar_size(request)
|
||||
self.assertEqual(size, "256")
|
||||
|
||||
def test_get_avatar_format(self):
|
||||
"""Test avatar format extraction."""
|
||||
request = self.factory.get("/avatar/test@example.com?d=jpg")
|
||||
format_type = self.middleware._get_avatar_format(request)
|
||||
self.assertEqual(format_type, "jpg")
|
||||
|
||||
def test_get_avatar_email(self):
|
||||
"""Test email extraction from avatar request."""
|
||||
request = self.factory.get("/avatar/test@example.com")
|
||||
email = self.middleware._get_avatar_email(request)
|
||||
self.assertEqual(email, "test@example.com")
|
||||
|
||||
|
||||
class AvatarMetricsTest(TestCase):
|
||||
"""Test AvatarMetrics class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.metrics = AvatarMetrics()
|
||||
|
||||
@patch("ivatar.opentelemetry_middleware.get_meter")
|
||||
def test_metrics_enabled(self, mock_get_meter):
|
||||
"""Test metrics when OpenTelemetry is enabled."""
|
||||
mock_meter = MagicMock()
|
||||
mock_counter = MagicMock()
|
||||
mock_histogram = MagicMock()
|
||||
|
||||
mock_meter.create_counter.return_value = mock_counter
|
||||
mock_meter.create_histogram.return_value = mock_histogram
|
||||
mock_get_meter.return_value = mock_meter
|
||||
|
||||
avatar_metrics = AvatarMetrics()
|
||||
|
||||
# Test avatar generation recording
|
||||
avatar_metrics.record_avatar_generated("128", "png", "generated")
|
||||
mock_counter.add.assert_called_with(
|
||||
1, {"size": "128", "format": "png", "source": "generated"}
|
||||
)
|
||||
|
||||
# Test cache hit recording
|
||||
avatar_metrics.record_cache_hit("128", "png")
|
||||
mock_counter.add.assert_called_with(1, {"size": "128", "format": "png"})
|
||||
|
||||
# Test file upload recording
|
||||
avatar_metrics.record_file_upload(1024, "image/png", True)
|
||||
mock_histogram.record.assert_called_with(
|
||||
1024, {"content_type": "image/png", "success": "True"}
|
||||
)
|
||||
|
||||
|
||||
class TracingDecoratorsTest(TestCase):
|
||||
"""Test tracing decorators."""
|
||||
|
||||
@patch("ivatar.opentelemetry_middleware.get_tracer")
|
||||
def test_trace_avatar_operation(self, mock_get_tracer):
|
||||
"""Test trace_avatar_operation decorator."""
|
||||
mock_tracer = MagicMock()
|
||||
mock_span = MagicMock()
|
||||
mock_tracer.start_as_current_span.return_value.__enter__.return_value = (
|
||||
mock_span
|
||||
)
|
||||
mock_get_tracer.return_value = mock_tracer
|
||||
|
||||
@trace_avatar_operation("test_operation")
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
|
||||
self.assertEqual(result, "success")
|
||||
mock_tracer.start_as_current_span.assert_called_once_with(
|
||||
"avatar.test_operation"
|
||||
)
|
||||
mock_span.set_status.assert_called_once()
|
||||
|
||||
@patch("ivatar.opentelemetry_middleware.get_tracer")
|
||||
def test_trace_avatar_operation_exception(self, mock_get_tracer):
|
||||
"""Test trace_avatar_operation decorator with exception."""
|
||||
mock_tracer = MagicMock()
|
||||
mock_span = MagicMock()
|
||||
mock_tracer.start_as_current_span.return_value.__enter__.return_value = (
|
||||
mock_span
|
||||
)
|
||||
mock_get_tracer.return_value = mock_tracer
|
||||
|
||||
@trace_avatar_operation("test_operation")
|
||||
def test_function():
|
||||
raise ValueError("test error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
test_function()
|
||||
|
||||
mock_span.set_status.assert_called_once()
|
||||
mock_span.set_attribute.assert_called_with("error.message", "test error")
|
||||
|
||||
def test_trace_file_upload(self):
|
||||
"""Test trace_file_upload decorator."""
|
||||
|
||||
@trace_file_upload("test_upload")
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
def test_trace_authentication(self):
|
||||
"""Test trace_authentication decorator."""
|
||||
|
||||
@trace_authentication("test_auth")
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
|
||||
class IntegrationTest(TestCase):
|
||||
"""Integration tests for OpenTelemetry."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = os.environ.copy()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
os.environ.clear()
|
||||
os.environ.update(self.original_env)
|
||||
|
||||
@patch("ivatar.opentelemetry_config.setup_opentelemetry")
|
||||
def test_setup_opentelemetry_called(self, mock_setup):
|
||||
"""Test that setup_opentelemetry is called during Django startup."""
|
||||
# This would be called during Django settings import
|
||||
from ivatar.opentelemetry_config import setup_opentelemetry as setup_func
|
||||
|
||||
setup_func()
|
||||
mock_setup.assert_called_once()
|
||||
|
||||
def test_is_enabled_function(self):
|
||||
"""Test is_enabled function."""
|
||||
# OpenTelemetry is now always enabled
|
||||
self.assertTrue(is_enabled())
|
||||
|
||||
# Test enabled with environment variable
|
||||
os.environ["OTEL_ENABLED"] = "true"
|
||||
config = OpenTelemetryConfig()
|
||||
self.assertTrue(config.enabled)
|
||||
|
||||
|
||||
class OpenTelemetryDisabledTest(TestCase):
|
||||
"""Test OpenTelemetry behavior when disabled (no-op mode)."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.original_env = os.environ.copy()
|
||||
# Ensure OpenTelemetry is disabled
|
||||
os.environ.pop("ENABLE_OPENTELEMETRY", None)
|
||||
os.environ.pop("OTEL_ENABLED", None)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
os.environ.clear()
|
||||
os.environ.update(self.original_env)
|
||||
|
||||
def test_opentelemetry_always_enabled(self):
|
||||
"""Test that OpenTelemetry instrumentation is always enabled."""
|
||||
# OpenTelemetry instrumentation is now always enabled
|
||||
self.assertTrue(is_enabled())
|
||||
|
||||
def test_decorators_work(self):
|
||||
"""Test that decorators work when OpenTelemetry is enabled."""
|
||||
|
||||
@trace_avatar_operation("test_operation")
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
def test_metrics_work(self):
|
||||
"""Test that metrics work when OpenTelemetry is enabled."""
|
||||
avatar_metrics = get_avatar_metrics()
|
||||
|
||||
# These should not raise exceptions
|
||||
avatar_metrics.record_avatar_generated("80", "png", "uploaded")
|
||||
avatar_metrics.record_cache_hit("80", "png")
|
||||
avatar_metrics.record_cache_miss("80", "png")
|
||||
avatar_metrics.record_external_request("gravatar", 200)
|
||||
avatar_metrics.record_file_upload(1024, "image/png", True)
|
||||
|
||||
def test_middleware_enabled(self):
|
||||
"""Test that middleware works when OpenTelemetry is enabled."""
|
||||
factory = RequestFactory()
|
||||
middleware = OpenTelemetryMiddleware(lambda r: HttpResponse("test"))
|
||||
|
||||
request = factory.get("/avatar/test@example.com")
|
||||
response = middleware(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content.decode(), "test")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user