mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 07:31:08 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
5
backend/tests/middleware/__init__.py
Normal file
5
backend/tests/middleware/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Middleware tests.
|
||||
|
||||
This module contains tests for custom middleware classes.
|
||||
"""
|
||||
368
backend/tests/middleware/test_contract_validation_middleware.py
Normal file
368
backend/tests/middleware/test_contract_validation_middleware.py
Normal file
@@ -0,0 +1,368 @@
|
||||
"""
|
||||
Tests for ContractValidationMiddleware.
|
||||
|
||||
Following Django styleguide pattern: test__<context>__<action>__<expected_outcome>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from django.test import TestCase, RequestFactory, override_settings
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
|
||||
from apps.api.v1.middleware import (
|
||||
ContractValidationMiddleware,
|
||||
ContractValidationSettings,
|
||||
)
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareInit(TestCase):
|
||||
"""Tests for ContractValidationMiddleware initialization."""
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test__init__debug_true__enables_middleware(self):
|
||||
"""Test middleware is enabled when DEBUG=True."""
|
||||
get_response = Mock()
|
||||
middleware = ContractValidationMiddleware(get_response)
|
||||
|
||||
assert middleware.enabled is True
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
def test__init__debug_false__disables_middleware(self):
|
||||
"""Test middleware is disabled when DEBUG=False."""
|
||||
get_response = Mock()
|
||||
middleware = ContractValidationMiddleware(get_response)
|
||||
|
||||
assert middleware.enabled is False
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareProcessResponse(TestCase):
|
||||
"""Tests for ContractValidationMiddleware.process_response."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
self.middleware.enabled = True
|
||||
|
||||
def test__process_response__non_api_path__skips_validation(self):
|
||||
"""Test process_response skips non-API paths."""
|
||||
request = self.factory.get("/some/path/")
|
||||
response = JsonResponse({"data": "value"})
|
||||
|
||||
result = self.middleware.process_response(request, response)
|
||||
|
||||
assert result == response
|
||||
|
||||
def test__process_response__non_json_response__skips_validation(self):
|
||||
"""Test process_response skips non-JSON responses."""
|
||||
request = self.factory.get("/api/v1/parks/")
|
||||
response = HttpResponse("HTML content")
|
||||
|
||||
result = self.middleware.process_response(request, response)
|
||||
|
||||
assert result == response
|
||||
|
||||
def test__process_response__error_status_code__skips_validation(self):
|
||||
"""Test process_response skips error responses."""
|
||||
request = self.factory.get("/api/v1/parks/")
|
||||
response = JsonResponse({"error": "Not found"}, status=404)
|
||||
|
||||
result = self.middleware.process_response(request, response)
|
||||
|
||||
assert result == response
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
def test__process_response__middleware_disabled__skips_validation(self):
|
||||
"""Test process_response skips when middleware is disabled."""
|
||||
self.middleware.enabled = False
|
||||
request = self.factory.get("/api/v1/parks/")
|
||||
response = JsonResponse({"data": "value"})
|
||||
|
||||
result = self.middleware.process_response(request, response)
|
||||
|
||||
assert result == response
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareFilterValidation(TestCase):
|
||||
"""Tests for filter metadata validation."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
self.middleware.enabled = True
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_filter_metadata__valid_categorical_filters__no_violation(
|
||||
self, mock_log
|
||||
):
|
||||
"""Test valid categorical filter format doesn't log violation."""
|
||||
request = self.factory.get("/api/v1/parks/filter-options/")
|
||||
valid_data = {
|
||||
"categorical": {
|
||||
"status": [
|
||||
{"value": "OPERATING", "label": "Operating", "count": 10},
|
||||
{"value": "CLOSED", "label": "Closed", "count": 5},
|
||||
]
|
||||
}
|
||||
}
|
||||
response = JsonResponse(valid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
# Should not log CATEGORICAL_OPTION_IS_STRING
|
||||
for call in mock_log.call_args_list:
|
||||
assert "CATEGORICAL_OPTION_IS_STRING" not in str(call)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_filter_metadata__string_options__logs_violation(self, mock_log):
|
||||
"""Test string filter options logs contract violation."""
|
||||
request = self.factory.get("/api/v1/parks/filter-options/")
|
||||
invalid_data = {
|
||||
"categorical": {
|
||||
"status": ["OPERATING", "CLOSED"] # Strings instead of objects
|
||||
}
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
# Should log CATEGORICAL_OPTION_IS_STRING violation
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("CATEGORICAL_OPTION_IS_STRING" in arg for arg in call_args)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_filter_metadata__missing_value_property__logs_violation(
|
||||
self, mock_log
|
||||
):
|
||||
"""Test filter option missing 'value' property logs violation."""
|
||||
request = self.factory.get("/api/v1/parks/filter-options/")
|
||||
invalid_data = {
|
||||
"categorical": {
|
||||
"status": [
|
||||
{"label": "Operating", "count": 10} # Missing 'value'
|
||||
]
|
||||
}
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("MISSING_VALUE_PROPERTY" in arg for arg in call_args)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_filter_metadata__missing_label_property__logs_violation(
|
||||
self, mock_log
|
||||
):
|
||||
"""Test filter option missing 'label' property logs violation."""
|
||||
request = self.factory.get("/api/v1/parks/filter-options/")
|
||||
invalid_data = {
|
||||
"categorical": {
|
||||
"status": [
|
||||
{"value": "OPERATING", "count": 10} # Missing 'label'
|
||||
]
|
||||
}
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("MISSING_LABEL_PROPERTY" in arg for arg in call_args)
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareRangeValidation(TestCase):
|
||||
"""Tests for range filter validation."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
self.middleware.enabled = True
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_range_filter__valid_range__no_violation(self, mock_log):
|
||||
"""Test valid range filter format doesn't log violation."""
|
||||
request = self.factory.get("/api/v1/rides/filter-options/")
|
||||
valid_data = {
|
||||
"ranges": {
|
||||
"height": {"min": 0, "max": 500, "step": 10, "unit": "ft"}
|
||||
}
|
||||
}
|
||||
response = JsonResponse(valid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
# Should not log RANGE_FILTER_NOT_OBJECT
|
||||
for call in mock_log.call_args_list:
|
||||
assert "RANGE_FILTER_NOT_OBJECT" not in str(call)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_range_filter__missing_min_max__logs_violation(self, mock_log):
|
||||
"""Test range filter missing min/max logs violation."""
|
||||
request = self.factory.get("/api/v1/rides/filter-options/")
|
||||
invalid_data = {
|
||||
"ranges": {
|
||||
"height": {"step": 10} # Missing 'min' and 'max'
|
||||
}
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("MISSING_RANGE_PROPERTY" in arg for arg in call_args)
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareHybridValidation(TestCase):
|
||||
"""Tests for hybrid response validation."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
self.middleware.enabled = True
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_hybrid_response__valid_strategy__no_violation(self, mock_log):
|
||||
"""Test valid hybrid response strategy doesn't log violation."""
|
||||
request = self.factory.get("/api/v1/parks/hybrid/")
|
||||
valid_data = {
|
||||
"strategy": "client_side",
|
||||
"data": [],
|
||||
"filter_metadata": {}
|
||||
}
|
||||
response = JsonResponse(valid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
# Should not log INVALID_STRATEGY_VALUE
|
||||
for call in mock_log.call_args_list:
|
||||
assert "INVALID_STRATEGY_VALUE" not in str(call)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_hybrid_response__invalid_strategy__logs_violation(
|
||||
self, mock_log
|
||||
):
|
||||
"""Test invalid hybrid strategy logs violation."""
|
||||
request = self.factory.get("/api/v1/parks/hybrid/")
|
||||
invalid_data = {
|
||||
"strategy": "invalid_strategy", # Not 'client_side' or 'server_side'
|
||||
"data": []
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("INVALID_STRATEGY_VALUE" in arg for arg in call_args)
|
||||
|
||||
|
||||
class TestContractValidationMiddlewarePaginationValidation(TestCase):
|
||||
"""Tests for pagination response validation."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
self.middleware.enabled = True
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_pagination__valid_response__no_violation(self, mock_log):
|
||||
"""Test valid pagination response doesn't log violation."""
|
||||
request = self.factory.get("/api/v1/parks/")
|
||||
valid_data = {
|
||||
"count": 10,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [{"id": 1}, {"id": 2}]
|
||||
}
|
||||
response = JsonResponse(valid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
# Should not log MISSING_PAGINATION_FIELD or RESULTS_NOT_ARRAY
|
||||
for call in mock_log.call_args_list:
|
||||
assert "MISSING_PAGINATION_FIELD" not in str(call)
|
||||
assert "RESULTS_NOT_ARRAY" not in str(call)
|
||||
|
||||
@patch.object(ContractValidationMiddleware, "_log_contract_violation")
|
||||
def test__validate_pagination__results_not_array__logs_violation(self, mock_log):
|
||||
"""Test pagination with non-array results logs violation."""
|
||||
request = self.factory.get("/api/v1/parks/")
|
||||
invalid_data = {
|
||||
"count": 10,
|
||||
"results": "not an array" # Should be array
|
||||
}
|
||||
response = JsonResponse(invalid_data)
|
||||
|
||||
self.middleware.process_response(request, response)
|
||||
|
||||
mock_log.assert_called()
|
||||
call_args = [str(call) for call in mock_log.call_args_list]
|
||||
assert any("RESULTS_NOT_ARRAY" in arg for arg in call_args)
|
||||
|
||||
|
||||
class TestContractValidationSettings(TestCase):
|
||||
"""Tests for ContractValidationSettings."""
|
||||
|
||||
def test__should_validate_path__regular_api_path__returns_true(self):
|
||||
"""Test should_validate_path returns True for regular API paths."""
|
||||
result = ContractValidationSettings.should_validate_path("/api/v1/parks/")
|
||||
|
||||
assert result is True
|
||||
|
||||
def test__should_validate_path__docs_path__returns_false(self):
|
||||
"""Test should_validate_path returns False for docs paths."""
|
||||
result = ContractValidationSettings.should_validate_path("/api/docs/")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test__should_validate_path__schema_path__returns_false(self):
|
||||
"""Test should_validate_path returns False for schema paths."""
|
||||
result = ContractValidationSettings.should_validate_path("/api/schema/")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test__should_validate_path__auth_path__returns_false(self):
|
||||
"""Test should_validate_path returns False for auth paths."""
|
||||
result = ContractValidationSettings.should_validate_path("/api/v1/auth/login/")
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestContractValidationMiddlewareViolationSuggestions(TestCase):
|
||||
"""Tests for violation suggestion messages."""
|
||||
|
||||
def setUp(self):
|
||||
self.get_response = Mock()
|
||||
self.middleware = ContractValidationMiddleware(self.get_response)
|
||||
|
||||
def test__get_violation_suggestion__categorical_string__returns_suggestion(self):
|
||||
"""Test get_violation_suggestion returns suggestion for CATEGORICAL_OPTION_IS_STRING."""
|
||||
suggestion = self.middleware._get_violation_suggestion(
|
||||
"CATEGORICAL_OPTION_IS_STRING"
|
||||
)
|
||||
|
||||
assert "ensure_filter_option_format" in suggestion
|
||||
assert "object arrays" in suggestion
|
||||
|
||||
def test__get_violation_suggestion__missing_value__returns_suggestion(self):
|
||||
"""Test get_violation_suggestion returns suggestion for MISSING_VALUE_PROPERTY."""
|
||||
suggestion = self.middleware._get_violation_suggestion("MISSING_VALUE_PROPERTY")
|
||||
|
||||
assert "value" in suggestion
|
||||
assert "FilterOptionSerializer" in suggestion
|
||||
|
||||
def test__get_violation_suggestion__unknown_violation__returns_default(self):
|
||||
"""Test get_violation_suggestion returns default for unknown violation."""
|
||||
suggestion = self.middleware._get_violation_suggestion("UNKNOWN_VIOLATION_TYPE")
|
||||
|
||||
assert "TypeScript interfaces" in suggestion
|
||||
Reference in New Issue
Block a user