# tests/unit/middleware/test_cloudflare.py """ Unit tests for CloudFlareMiddleware, get_real_client_ip, and get_client_country. Tests cover: - CloudFlare dispatch: CF disabled bypass, header extraction, missing header fallbacks, CF-Visitor JSON parsing (valid/malformed), X-CF-Ray response header, no-client fallback - get_real_client_ip: state.real_ip, CF-Connecting-IP, X-Forwarded-For, request.client fallback, no client → "unknown" - get_client_country: state.client_country, CF-IPCountry header, neither available, state takes priority over header """ from unittest.mock import AsyncMock, Mock, patch import pytest from starlette.requests import Request from middleware.cloudflare import ( CloudFlareMiddleware, get_client_country, get_real_client_ip, ) # --------------------------------------------------------------------------- # TestCloudFlareDispatch # --------------------------------------------------------------------------- @pytest.mark.unit class TestCloudFlareDispatch: """Tests for CloudFlareMiddleware.dispatch().""" @pytest.mark.asyncio async def test_cf_disabled_bypasses_middleware(self): """When cloudflare_enabled is False, call_next is invoked directly.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.state = Mock(spec=[]) response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = False result = await middleware.dispatch(request, call_next) call_next.assert_awaited_once_with(request) assert result is response # No state attributes should have been set assert not hasattr(request.state, "real_ip") @pytest.mark.asyncio async def test_all_cf_headers_extracted(self): """All CloudFlare headers are stored on request.state.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = { "CF-Connecting-IP": "1.2.3.4", "CF-IPCountry": "LU", "CF-Ray": "abc123", "CF-Visitor": '{"scheme":"https"}', } request.state = Mock(spec=[]) request.client = Mock(host="127.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) assert request.state.real_ip == "1.2.3.4" assert request.state.client_country == "LU" assert request.state.cf_ray == "abc123" assert request.state.original_scheme == "https" @pytest.mark.asyncio async def test_missing_cf_connecting_ip_falls_back_to_client(self): """Without CF-Connecting-IP, real_ip falls back to request.client.host.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {} request.state = Mock(spec=[]) request.client = Mock(host="10.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) assert request.state.real_ip == "10.0.0.1" @pytest.mark.asyncio async def test_missing_cf_headers_no_state_attributes(self): """Missing optional headers do not set corresponding state attributes.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {} request.state = Mock(spec=[]) request.client = Mock(host="10.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) # client_country and cf_ray should NOT be set assert not hasattr(request.state, "client_country") assert not hasattr(request.state, "cf_ray") assert not hasattr(request.state, "original_scheme") @pytest.mark.asyncio async def test_cf_visitor_json_parsed(self): """CF-Visitor JSON body is parsed and scheme extracted.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {"CF-Visitor": '{"scheme":"http"}'} request.state = Mock(spec=[]) request.client = Mock(host="127.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) assert request.state.original_scheme == "http" @pytest.mark.asyncio async def test_cf_visitor_malformed_json_defaults_to_https(self): """Malformed CF-Visitor JSON defaults original_scheme to 'https'.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {"CF-Visitor": "not-json"} request.state = Mock(spec=[]) request.client = Mock(host="127.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) assert request.state.original_scheme == "https" @pytest.mark.asyncio async def test_cf_ray_added_to_response_header(self): """X-CF-Ray response header is set when CF-Ray is present.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {"CF-Ray": "ray-999"} request.state = Mock(spec=[]) request.client = Mock(host="127.0.0.1") response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True result = await middleware.dispatch(request, call_next) assert result.headers["X-CF-Ray"] == "ray-999" @pytest.mark.asyncio async def test_no_client_fallback_returns_unknown(self): """When request.client is None, real_ip is set to 'unknown'.""" middleware = CloudFlareMiddleware(app=None) request = Mock(spec=Request) request.headers = {} request.state = Mock(spec=[]) request.client = None response = Mock(status_code=200, headers={}) call_next = AsyncMock(return_value=response) with patch("middleware.cloudflare.settings") as mock_settings: mock_settings.cloudflare_enabled = True await middleware.dispatch(request, call_next) assert request.state.real_ip == "unknown" # --------------------------------------------------------------------------- # TestGetRealClientIp # --------------------------------------------------------------------------- @pytest.mark.unit class TestGetRealClientIp: """Tests for get_real_client_ip() standalone helper.""" def test_returns_state_real_ip_when_present(self): """state.real_ip takes priority over everything else.""" request = Mock(spec=Request) request.state = Mock() request.state.real_ip = "5.6.7.8" request.headers = {"CF-Connecting-IP": "1.2.3.4"} assert get_real_client_ip(request) == "5.6.7.8" def test_returns_cf_connecting_ip_header(self): """Falls back to CF-Connecting-IP when state.real_ip absent.""" request = Mock(spec=Request) request.state = Mock(spec=[]) # no real_ip attribute request.headers = {"CF-Connecting-IP": "1.2.3.4"} assert get_real_client_ip(request) == "1.2.3.4" def test_returns_first_x_forwarded_for_ip(self): """Falls back to first IP from X-Forwarded-For header.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {"X-Forwarded-For": "9.8.7.6, 10.0.0.1, 10.0.0.2"} assert get_real_client_ip(request) == "9.8.7.6" def test_returns_single_x_forwarded_for_ip(self): """X-Forwarded-For with single IP still works.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {"X-Forwarded-For": "9.8.7.6"} assert get_real_client_ip(request) == "9.8.7.6" def test_returns_client_host_as_fallback(self): """Falls back to request.client.host when no proxy headers.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {} request.client = Mock(host="192.168.1.1") assert get_real_client_ip(request) == "192.168.1.1" def test_returns_unknown_when_no_client(self): """Returns 'unknown' when client is None and no proxy headers.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {} request.client = None assert get_real_client_ip(request) == "unknown" # --------------------------------------------------------------------------- # TestGetClientCountry # --------------------------------------------------------------------------- @pytest.mark.unit class TestGetClientCountry: """Tests for get_client_country() standalone helper.""" def test_returns_state_client_country_when_present(self): """state.client_country takes priority.""" request = Mock(spec=Request) request.state = Mock() request.state.client_country = "LU" request.headers = {"CF-IPCountry": "DE"} assert get_client_country(request) == "LU" def test_returns_cf_ipcountry_header_fallback(self): """Falls back to CF-IPCountry header when state not set.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {"CF-IPCountry": "FR"} assert get_client_country(request) == "FR" def test_returns_none_when_neither_available(self): """Returns None when neither state nor header is available.""" request = Mock(spec=Request) request.state = Mock(spec=[]) request.headers = {} assert get_client_country(request) is None def test_state_takes_priority_over_header(self): """state.client_country takes priority even when header differs.""" request = Mock(spec=Request) request.state = Mock() request.state.client_country = "BE" request.headers = {"CF-IPCountry": "NL"} assert get_client_country(request) == "BE" def test_header_with_no_state_attribute(self): """CF-IPCountry header used when state has no client_country attr.""" request = Mock(spec=Request) # Use spec=[] so hasattr returns False for all attributes request.state = Mock(spec=[]) request.headers = {"CF-IPCountry": "DE"} assert get_client_country(request) == "DE"