Spaces:
Sleeping
Sleeping
| """Tests for server cache control middleware""" | |
| import pytest | |
| from aiohttp import web | |
| from aiohttp.test_utils import make_mocked_request | |
| from typing import Dict, Any | |
| from middleware.cache_middleware import cache_control, ONE_HOUR, ONE_DAY, IMG_EXTENSIONS | |
| pytestmark = pytest.mark.asyncio # Apply asyncio mark to all tests | |
| # Test configuration data | |
| CACHE_SCENARIOS = [ | |
| # Image file scenarios | |
| { | |
| "name": "image_200_status", | |
| "path": "/test.jpg", | |
| "status": 200, | |
| "expected_cache": f"public, max-age={ONE_DAY}", | |
| "should_have_header": True, | |
| }, | |
| { | |
| "name": "image_404_status", | |
| "path": "/missing.jpg", | |
| "status": 404, | |
| "expected_cache": f"public, max-age={ONE_HOUR}", | |
| "should_have_header": True, | |
| }, | |
| # JavaScript/CSS scenarios | |
| { | |
| "name": "js_no_cache", | |
| "path": "/script.js", | |
| "status": 200, | |
| "expected_cache": "no-cache", | |
| "should_have_header": True, | |
| }, | |
| { | |
| "name": "css_no_cache", | |
| "path": "/styles.css", | |
| "status": 200, | |
| "expected_cache": "no-cache", | |
| "should_have_header": True, | |
| }, | |
| { | |
| "name": "index_json_no_cache", | |
| "path": "/api/index.json", | |
| "status": 200, | |
| "expected_cache": "no-cache", | |
| "should_have_header": True, | |
| }, | |
| { | |
| "name": "localized_index_json_no_cache", | |
| "path": "/templates/index.zh.json", | |
| "status": 200, | |
| "expected_cache": "no-cache", | |
| "should_have_header": True, | |
| }, | |
| # Non-matching files | |
| { | |
| "name": "html_no_header", | |
| "path": "/index.html", | |
| "status": 200, | |
| "expected_cache": None, | |
| "should_have_header": False, | |
| }, | |
| { | |
| "name": "txt_no_header", | |
| "path": "/data.txt", | |
| "status": 200, | |
| "expected_cache": None, | |
| "should_have_header": False, | |
| }, | |
| { | |
| "name": "api_endpoint_no_header", | |
| "path": "/api/endpoint", | |
| "status": 200, | |
| "expected_cache": None, | |
| "should_have_header": False, | |
| }, | |
| { | |
| "name": "pdf_no_header", | |
| "path": "/file.pdf", | |
| "status": 200, | |
| "expected_cache": None, | |
| "should_have_header": False, | |
| }, | |
| ] | |
| # Status code scenarios for images | |
| IMAGE_STATUS_SCENARIOS = [ | |
| # Success statuses get long cache | |
| {"status": 200, "expected": f"public, max-age={ONE_DAY}"}, | |
| {"status": 201, "expected": f"public, max-age={ONE_DAY}"}, | |
| {"status": 202, "expected": f"public, max-age={ONE_DAY}"}, | |
| {"status": 204, "expected": f"public, max-age={ONE_DAY}"}, | |
| {"status": 206, "expected": f"public, max-age={ONE_DAY}"}, | |
| # Permanent redirects get long cache | |
| {"status": 301, "expected": f"public, max-age={ONE_DAY}"}, | |
| {"status": 308, "expected": f"public, max-age={ONE_DAY}"}, | |
| # Temporary redirects get no cache | |
| {"status": 302, "expected": "no-cache"}, | |
| {"status": 303, "expected": "no-cache"}, | |
| {"status": 307, "expected": "no-cache"}, | |
| # 404 gets short cache | |
| {"status": 404, "expected": f"public, max-age={ONE_HOUR}"}, | |
| ] | |
| # Case sensitivity test paths | |
| CASE_SENSITIVITY_PATHS = ["/image.JPG", "/photo.PNG", "/pic.JpEg"] | |
| # Edge case test paths | |
| EDGE_CASE_PATHS = [ | |
| { | |
| "name": "query_strings_ignored", | |
| "path": "/image.jpg?v=123&size=large", | |
| "expected": f"public, max-age={ONE_DAY}", | |
| }, | |
| { | |
| "name": "multiple_dots_in_path", | |
| "path": "/image.min.jpg", | |
| "expected": f"public, max-age={ONE_DAY}", | |
| }, | |
| { | |
| "name": "nested_paths_with_images", | |
| "path": "/static/images/photo.jpg", | |
| "expected": f"public, max-age={ONE_DAY}", | |
| }, | |
| ] | |
| class TestCacheControl: | |
| """Test cache control middleware functionality""" | |
| def status_handler_factory(self): | |
| """Create a factory for handlers that return specific status codes""" | |
| def factory(status: int, headers: Dict[str, str] = None): | |
| async def handler(request): | |
| return web.Response(status=status, headers=headers or {}) | |
| return handler | |
| return factory | |
| def mock_handler(self, status_handler_factory): | |
| """Create a mock handler that returns a response with 200 status""" | |
| return status_handler_factory(200) | |
| def handler_with_existing_cache(self, status_handler_factory): | |
| """Create a handler that returns response with existing Cache-Control header""" | |
| return status_handler_factory(200, {"Cache-Control": "max-age=3600"}) | |
| async def assert_cache_header( | |
| self, | |
| response: web.Response, | |
| expected_cache: str = None, | |
| should_have_header: bool = True, | |
| ): | |
| """Helper to assert cache control headers""" | |
| if should_have_header: | |
| assert "Cache-Control" in response.headers | |
| if expected_cache: | |
| assert response.headers["Cache-Control"] == expected_cache | |
| else: | |
| assert "Cache-Control" not in response.headers | |
| # Parameterized tests | |
| async def test_cache_control_scenarios( | |
| self, scenario: Dict[str, Any], status_handler_factory | |
| ): | |
| """Test various cache control scenarios""" | |
| handler = status_handler_factory(scenario["status"]) | |
| request = make_mocked_request("GET", scenario["path"]) | |
| response = await cache_control(request, handler) | |
| assert response.status == scenario["status"] | |
| await self.assert_cache_header( | |
| response, scenario["expected_cache"], scenario["should_have_header"] | |
| ) | |
| async def test_all_image_extensions(self, ext: str, mock_handler): | |
| """Test all defined image extensions are handled correctly""" | |
| request = make_mocked_request("GET", f"/image{ext}") | |
| response = await cache_control(request, mock_handler) | |
| assert response.status == 200 | |
| assert "Cache-Control" in response.headers | |
| assert response.headers["Cache-Control"] == f"public, max-age={ONE_DAY}" | |
| async def test_image_status_codes( | |
| self, status_scenario: Dict[str, Any], status_handler_factory | |
| ): | |
| """Test different status codes for image requests""" | |
| handler = status_handler_factory(status_scenario["status"]) | |
| request = make_mocked_request("GET", "/image.jpg") | |
| response = await cache_control(request, handler) | |
| assert response.status == status_scenario["status"] | |
| assert "Cache-Control" in response.headers | |
| assert response.headers["Cache-Control"] == status_scenario["expected"] | |
| async def test_case_insensitive_image_extension(self, path: str, mock_handler): | |
| """Test that image extensions are matched case-insensitively""" | |
| request = make_mocked_request("GET", path) | |
| response = await cache_control(request, mock_handler) | |
| assert "Cache-Control" in response.headers | |
| assert response.headers["Cache-Control"] == f"public, max-age={ONE_DAY}" | |
| async def test_edge_cases(self, edge_case: Dict[str, str], mock_handler): | |
| """Test edge cases like query strings, nested paths, etc.""" | |
| request = make_mocked_request("GET", edge_case["path"]) | |
| response = await cache_control(request, mock_handler) | |
| assert "Cache-Control" in response.headers | |
| assert response.headers["Cache-Control"] == edge_case["expected"] | |
| # Header preservation tests (special cases not covered by parameterization) | |
| async def test_js_preserves_existing_headers(self, handler_with_existing_cache): | |
| """Test that .js files preserve existing Cache-Control headers""" | |
| request = make_mocked_request("GET", "/script.js") | |
| response = await cache_control(request, handler_with_existing_cache) | |
| # setdefault should preserve existing header | |
| assert response.headers["Cache-Control"] == "max-age=3600" | |
| async def test_css_preserves_existing_headers(self, handler_with_existing_cache): | |
| """Test that .css files preserve existing Cache-Control headers""" | |
| request = make_mocked_request("GET", "/styles.css") | |
| response = await cache_control(request, handler_with_existing_cache) | |
| # setdefault should preserve existing header | |
| assert response.headers["Cache-Control"] == "max-age=3600" | |
| async def test_image_preserves_existing_headers(self, status_handler_factory): | |
| """Test that image cache headers preserve existing Cache-Control""" | |
| handler = status_handler_factory(200, {"Cache-Control": "private, no-cache"}) | |
| request = make_mocked_request("GET", "/image.jpg") | |
| response = await cache_control(request, handler) | |
| # setdefault should preserve existing header | |
| assert response.headers["Cache-Control"] == "private, no-cache" | |
| async def test_304_not_modified_inherits_cache(self, status_handler_factory): | |
| """Test that 304 Not Modified doesn't set cache headers for images""" | |
| handler = status_handler_factory(304, {"Cache-Control": "max-age=7200"}) | |
| request = make_mocked_request("GET", "/not-modified.jpg") | |
| response = await cache_control(request, handler) | |
| assert response.status == 304 | |
| # Should preserve existing cache header, not override | |
| assert response.headers["Cache-Control"] == "max-age=7200" | |