Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -0,0 +1,5 @@
"""
Middleware tests.
This module contains tests for custom middleware classes.
"""

View 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