# Copyright (c) 2023-present Plane Software, Inc. and contributors # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. """ Unit tests for ReadReplicaRoutingMiddleware. This module contains comprehensive tests for the ReadReplicaRoutingMiddleware that handles intelligent database routing to read replicas based on HTTP methods and view configuration. Test Organization: - TestReadReplicaRoutingMiddleware: Core middleware functionality - TestProcessView: process_view method behavior - TestReplicaDecisionLogic: Decision logic for replica usage - TestAttributeDetection: View attribute detection methods - TestExceptionHandling: Exception handling and cleanup - TestRealViewIntegration: Real Django/DRF view integration - TestEdgeCases: Edge cases and error conditions """ import pytest from unittest.mock import Mock, patch from django.http import HttpResponse from django.test import RequestFactory from django.views import View from rest_framework.views import APIView from rest_framework.viewsets import ViewSet from plane.middleware.db_routing import ReadReplicaRoutingMiddleware # Pytest fixtures @pytest.fixture def mock_get_response(): """Fixture for mocked get_response callable.""" return Mock(return_value=HttpResponse()) @pytest.fixture def middleware(mock_get_response): """Fixture for ReadReplicaRoutingMiddleware instance.""" return ReadReplicaRoutingMiddleware(mock_get_response) @pytest.fixture def request_factory(): """Fixture for Django RequestFactory.""" return RequestFactory() @pytest.fixture def mock_view_func(): """Fixture for a basic mocked view function.""" view = Mock() view.use_read_replica = True return view @pytest.fixture def get_request(request_factory): """Fixture for a GET request.""" return request_factory.get("/api/test/") @pytest.fixture def post_request(request_factory): """Fixture for a POST request.""" return request_factory.post("/api/test/") @pytest.mark.unit class TestReadReplicaRoutingMiddleware: """Test cases for ReadReplicaRoutingMiddleware core functionality.""" def test_middleware_initialization(self, middleware, mock_get_response): """Test middleware initializes correctly with expected attributes.""" assert middleware.get_response == mock_get_response assert hasattr(middleware, "READ_ONLY_METHODS") assert "GET" in middleware.READ_ONLY_METHODS assert "HEAD" in middleware.READ_ONLY_METHODS assert "OPTIONS" in middleware.READ_ONLY_METHODS def test_read_only_methods_constant(self, middleware): """Test READ_ONLY_METHODS contains expected HTTP methods.""" expected_methods = {"GET", "HEAD", "OPTIONS"} assert middleware.READ_ONLY_METHODS == expected_methods @patch("plane.middleware.db_routing.set_use_read_replica") @patch("plane.middleware.db_routing.clear_read_replica_context") def test_call_routes_write_methods_to_primary( self, mock_clear, mock_set, middleware, post_request, mock_get_response ): """Test __call__ routes write methods to primary database.""" response = middleware(post_request) mock_set.assert_called_once_with(False) # Primary database mock_clear.assert_called_once() assert response == mock_get_response.return_value @patch("plane.middleware.db_routing.clear_read_replica_context") def test_call_with_read_methods_waits_for_process_view( self, mock_clear, middleware, get_request, mock_get_response ): """Test __call__ with read methods waits for process_view.""" response = middleware(get_request) mock_clear.assert_called_once() assert response == mock_get_response.return_value @patch("plane.middleware.db_routing.clear_read_replica_context") def test_call_always_cleans_up_context(self, mock_clear, middleware, get_request): """Test __call__ always cleans up context.""" middleware(get_request) mock_clear.assert_called_once() @patch("plane.middleware.db_routing.clear_read_replica_context") def test_call_cleans_up_context_on_exception(self, mock_clear, middleware, get_request, mock_get_response): """Test __call__ cleans up context even if get_response raises.""" mock_get_response.side_effect = Exception("Test exception") with pytest.raises(Exception, match="Test exception"): middleware(get_request) mock_clear.assert_called_once() @pytest.mark.unit class TestProcessView: """Test cases for process_view method functionality.""" @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_read_method_and_replica_true(self, mock_set, middleware, get_request): """Test process_view with GET request and use_read_replica=True.""" view_func = Mock() view_func.use_read_replica = True result = middleware.process_view(get_request, view_func, (), {}) mock_set.assert_called_once_with(True) assert result is None @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_read_method_and_replica_false(self, mock_set, middleware, get_request): """Test process_view with GET request and use_read_replica=False.""" view_func = Mock() view_func.use_read_replica = False result = middleware.process_view(get_request, view_func, (), {}) mock_set.assert_called_once_with(False) assert result is None @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_read_method_and_no_replica_attribute(self, mock_set, middleware, get_request): """Test process_view with GET request and no use_read_replica attr.""" view_func = Mock(spec=[]) # No use_read_replica attribute result = middleware.process_view(get_request, view_func, (), {}) mock_set.assert_called_once_with(False) # Default to primary assert result is None def test_with_write_method_ignores_view_attributes(self, middleware, post_request): """Test process_view with write methods ignores view attributes.""" view_func = Mock() view_func.use_read_replica = True # This should be ignored for POST result = middleware.process_view(post_request, view_func, (), {}) assert result is None # Should not process for write methods @pytest.mark.unit class TestReplicaDecisionLogic: """Test cases for replica decision logic methods.""" def test_should_use_read_replica_with_true_attribute(self, middleware): """Test _should_use_read_replica returns True for True attribute.""" view_func = Mock() view_func.use_read_replica = True result = middleware._should_use_read_replica(view_func) assert result is True def test_should_use_read_replica_with_false_attribute(self, middleware): """Test _should_use_read_replica returns False for False attribute.""" view_func = Mock() view_func.use_read_replica = False result = middleware._should_use_read_replica(view_func) assert result is False def test_should_use_read_replica_with_no_attribute_defaults_false(self, middleware): """Test _should_use_read_replica defaults to False for missing attr.""" view_func = Mock(spec=[]) # No use_read_replica attribute result = middleware._should_use_read_replica(view_func) assert result is False @pytest.mark.unit class TestAttributeDetection: """Test cases for view attribute detection methods.""" def test_get_use_replica_attribute_function_based_view(self, middleware): """Test _get_use_replica_attribute with function-based view.""" # Test with True view_func = Mock() view_func.use_read_replica = True result = middleware._get_use_replica_attribute(view_func) assert result is True # Test with False view_func.use_read_replica = False result = middleware._get_use_replica_attribute(view_func) assert result is False # Test with no attribute view_func = Mock(spec=[]) result = middleware._get_use_replica_attribute(view_func) assert result is None def test_get_use_replica_attribute_django_cbv(self, middleware): """Test _get_use_replica_attribute with Django CBV wrapper.""" view_class = Mock() view_class.use_read_replica = True view_func = Mock() view_func.view_class = view_class # Remove use_read_replica from view_func to ensure it checks view_class del view_func.use_read_replica result = middleware._get_use_replica_attribute(view_func) assert result is True def test_get_use_replica_attribute_drf_wrapper(self, middleware): """Test _get_use_replica_attribute with DRF wrapper.""" # Create a real object to avoid Mock issues class ViewClass: use_read_replica = True class ViewFunc: cls = ViewClass() view_func = ViewFunc() result = middleware._get_use_replica_attribute(view_func) assert result is True def test_get_use_replica_attribute_priority_order(self, middleware): """Test attribute priority: direct > view_class > cls.""" view_func = Mock() view_func.use_read_replica = True # Direct attribute (highest priority) # Add conflicting attributes with lower priority view_class = Mock() view_class.use_read_replica = False view_func.view_class = view_class cls = Mock() cls.use_read_replica = False view_func.cls = cls result = middleware._get_use_replica_attribute(view_func) assert result is True # Should use direct attribute @pytest.mark.parametrize( "value,expected", [ (True, True), (False, False), (1, True), (0, False), ("yes", True), ("", False), ([], False), ([1], True), (None, False), ], ) def test_should_use_read_replica_truthy_falsy_values(self, middleware, value, expected): """Test _should_use_read_replica with various truthy/falsy values.""" # Create a real object to test the attribute handling class TestView: pass view_func = TestView() view_func.use_read_replica = value result = middleware._should_use_read_replica(view_func) assert result == expected @pytest.mark.unit class TestExceptionHandling: """Test cases for exception handling and cleanup.""" @patch("plane.middleware.db_routing.clear_read_replica_context") def test_process_exception_cleans_up_context(self, mock_clear, middleware, request_factory): """Test process_exception cleans up context.""" request = request_factory.get("/api/test/") exception = Exception("Test exception") result = middleware.process_exception(request, exception) mock_clear.assert_called_once() assert result is None # Don't handle the exception @patch("plane.middleware.db_routing.set_use_read_replica") @patch("plane.middleware.db_routing.clear_read_replica_context") def test_integration_full_request_cycle(self, mock_clear, mock_set, middleware, request_factory, mock_get_response): """Test complete request cycle from __call__ through process_view.""" request = request_factory.get("/api/test/") view_func = Mock() view_func.use_read_replica = True # Call middleware and process_view manually response = middleware(request) middleware.process_view(request, view_func, (), {}) mock_set.assert_called_once_with(True) mock_clear.assert_called_once() assert response == mock_get_response.return_value @pytest.mark.unit class TestRealViewIntegration: """Test middleware with real Django/DRF view classes.""" @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_django_class_based_view(self, mock_set, middleware, request_factory): """Test middleware with actual Django CBV.""" class TestView(View): use_read_replica = True # Simulate Django's URL resolver creating a view wrapper view_func = TestView.as_view() request = request_factory.get("/api/test/") middleware.process_view(request, view_func, (), {}) mock_set.assert_called_once_with(True) @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_drf_api_view(self, mock_set, middleware, request_factory): """Test middleware with DRF APIView.""" class TestAPIView(APIView): use_read_replica = True # Simulate DRF's URL pattern creating a view wrapper view_func = TestAPIView.as_view() request = request_factory.get("/api/test/") middleware.process_view(request, view_func, (), {}) mock_set.assert_called_once_with(True) @patch("plane.middleware.db_routing.set_use_read_replica") def test_with_drf_viewset(self, mock_set, middleware, request_factory): """Test middleware with DRF ViewSet.""" class TestViewSet(ViewSet): use_read_replica = True # Simulate DRF router creating viewset action view_func = TestViewSet.as_view({"get": "list"}) request = request_factory.get("/api/test/") middleware.process_view(request, view_func, (), {}) mock_set.assert_called_once_with(True) @pytest.mark.unit class TestEdgeCases: """Test edge cases and error conditions.""" def test_process_view_with_none_view_func(self, middleware, request_factory): """Test process_view handles None view_func gracefully.""" request = request_factory.get("/api/test/") result = middleware.process_view(request, None, (), {}) assert result is None # Should not crash def test_get_use_replica_attribute_with_attribute_error(self, middleware): """Test _get_use_replica_attribute with view that raises AttributeError.""" # Create a view class that raises AttributeError on access class ProblematicView: def __getattr__(self, name): if name == "use_read_replica": raise AttributeError("Simulated attribute error") raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") view_func = ProblematicView() result = middleware._get_use_replica_attribute(view_func) assert result is None # Should handle gracefully def test_multiple_exception_calls_are_safe(self, middleware, request_factory): """Test that multiple calls to process_exception don't cause issues.""" request = request_factory.get("/api/test/") exception = Exception("Test exception") # Call multiple times result1 = middleware.process_exception(request, exception) result2 = middleware.process_exception(request, exception) assert result1 is None # Both should return None safely assert result2 is None