- Replace black, isort, and flake8 with Ruff (all-in-one linter and formatter) - Add comprehensive pyproject.toml configuration - Simplify Makefile code quality targets - Configure exclusions for venv/.venv in pyproject.toml - Auto-fix 1,359 linting issues across codebase Benefits: - Much faster builds (Ruff is written in Rust) - Single tool replaces multiple tools - More comprehensive rule set (UP, B, C4, SIM, PIE, RET, Q) - All configuration centralized in pyproject.toml - Better import sorting and formatting consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
257 lines
8.5 KiB
Python
257 lines
8.5 KiB
Python
# tests/unit/middleware/test_theme_context.py
|
|
"""
|
|
Comprehensive unit tests for ThemeContextMiddleware and ThemeContextManager.
|
|
|
|
Tests cover:
|
|
- Theme loading and caching
|
|
- Default theme structure and validation
|
|
- Vendor-specific theme retrieval
|
|
- Fallback to default theme
|
|
- Middleware integration
|
|
- Edge cases and error handling
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
|
|
from middleware.theme_context import (
|
|
ThemeContextManager,
|
|
ThemeContextMiddleware,
|
|
get_current_theme,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestThemeContextManager:
|
|
"""Test suite for ThemeContextManager."""
|
|
|
|
def test_get_default_theme_structure(self):
|
|
"""Test default theme has correct structure."""
|
|
theme = ThemeContextManager.get_default_theme()
|
|
|
|
assert "theme_name" in theme
|
|
assert "colors" in theme
|
|
assert "fonts" in theme
|
|
assert "branding" in theme
|
|
assert "layout" in theme
|
|
assert "social_links" in theme
|
|
assert "css_variables" in theme
|
|
|
|
def test_get_default_theme_colors(self):
|
|
"""Test default theme has all required colors."""
|
|
theme = ThemeContextManager.get_default_theme()
|
|
|
|
required_colors = [
|
|
"primary",
|
|
"secondary",
|
|
"accent",
|
|
"background",
|
|
"text",
|
|
"border",
|
|
]
|
|
for color in required_colors:
|
|
assert color in theme["colors"]
|
|
assert theme["colors"][color].startswith("#")
|
|
|
|
def test_get_default_theme_fonts(self):
|
|
"""Test default theme has font configuration."""
|
|
theme = ThemeContextManager.get_default_theme()
|
|
|
|
assert "heading" in theme["fonts"]
|
|
assert "body" in theme["fonts"]
|
|
assert isinstance(theme["fonts"]["heading"], str)
|
|
assert isinstance(theme["fonts"]["body"], str)
|
|
|
|
def test_get_default_theme_branding(self):
|
|
"""Test default theme branding structure."""
|
|
theme = ThemeContextManager.get_default_theme()
|
|
|
|
assert "logo" in theme["branding"]
|
|
assert "logo_dark" in theme["branding"]
|
|
assert "favicon" in theme["branding"]
|
|
assert "banner" in theme["branding"]
|
|
|
|
def test_get_default_theme_css_variables(self):
|
|
"""Test default theme has CSS variables."""
|
|
theme = ThemeContextManager.get_default_theme()
|
|
|
|
assert "--color-primary" in theme["css_variables"]
|
|
assert "--font-heading" in theme["css_variables"]
|
|
assert "--font-body" in theme["css_variables"]
|
|
|
|
def test_get_vendor_theme_with_custom_theme(self):
|
|
"""Test getting vendor-specific theme."""
|
|
mock_db = Mock()
|
|
mock_theme = Mock()
|
|
|
|
# Mock to_dict to return actual dictionary
|
|
custom_theme_dict = {"theme_name": "custom", "colors": {"primary": "#ff0000"}}
|
|
mock_theme.to_dict.return_value = custom_theme_dict
|
|
|
|
# Correct filter chain: query().filter().first()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_theme
|
|
|
|
theme = ThemeContextManager.get_vendor_theme(mock_db, vendor_id=1)
|
|
|
|
assert theme["theme_name"] == "custom"
|
|
assert theme["colors"]["primary"] == "#ff0000"
|
|
mock_theme.to_dict.assert_called_once()
|
|
|
|
def test_get_vendor_theme_fallback_to_default(self):
|
|
"""Test falling back to default theme when no custom theme exists."""
|
|
mock_db = Mock()
|
|
# Correct filter chain: query().filter().first()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
theme = ThemeContextManager.get_vendor_theme(mock_db, vendor_id=1)
|
|
|
|
# Verify it returns a dict (not a Mock)
|
|
assert isinstance(theme, dict)
|
|
assert theme["theme_name"] == "default"
|
|
assert "colors" in theme
|
|
assert "fonts" in theme
|
|
|
|
def test_get_vendor_theme_inactive_theme(self):
|
|
"""Test that inactive themes are not returned."""
|
|
mock_db = Mock()
|
|
# Correct filter chain: query().filter().first()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
theme = ThemeContextManager.get_vendor_theme(mock_db, vendor_id=1)
|
|
|
|
# Should return default theme (actual dict)
|
|
assert isinstance(theme, dict)
|
|
assert theme["theme_name"] == "default"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestThemeContextMiddleware:
|
|
"""Test suite for ThemeContextMiddleware."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_middleware_loads_theme_for_vendor(self):
|
|
"""Test middleware loads theme when vendor exists."""
|
|
middleware = ThemeContextMiddleware(app=None)
|
|
|
|
request = Mock(spec=Request)
|
|
mock_vendor = Mock()
|
|
mock_vendor.id = 1
|
|
mock_vendor.name = "Test Vendor"
|
|
request.state = Mock(vendor=mock_vendor)
|
|
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
mock_db = MagicMock()
|
|
mock_theme = {"theme_name": "test_theme"}
|
|
|
|
with (
|
|
patch("middleware.theme_context.get_db", return_value=iter([mock_db])),
|
|
patch.object(
|
|
ThemeContextManager, "get_vendor_theme", return_value=mock_theme
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert request.state.theme == mock_theme
|
|
call_next.assert_called_once_with(request)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_middleware_uses_default_theme_no_vendor(self):
|
|
"""Test middleware uses default theme when no vendor."""
|
|
middleware = ThemeContextMiddleware(app=None)
|
|
|
|
request = Mock(spec=Request)
|
|
request.state = Mock(vendor=None)
|
|
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
assert hasattr(request.state, "theme")
|
|
assert request.state.theme["theme_name"] == "default"
|
|
call_next.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_middleware_handles_theme_loading_error(self):
|
|
"""Test middleware handles errors gracefully."""
|
|
middleware = ThemeContextMiddleware(app=None)
|
|
|
|
request = Mock(spec=Request)
|
|
mock_vendor = Mock(id=1, name="Test Vendor")
|
|
request.state = Mock(vendor=mock_vendor)
|
|
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
mock_db = MagicMock()
|
|
|
|
with (
|
|
patch("middleware.theme_context.get_db", return_value=iter([mock_db])),
|
|
patch.object(
|
|
ThemeContextManager,
|
|
"get_vendor_theme",
|
|
side_effect=Exception("DB Error"),
|
|
),
|
|
):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
# Should fallback to default theme
|
|
assert request.state.theme["theme_name"] == "default"
|
|
call_next.assert_called_once()
|
|
|
|
def test_get_current_theme_exists(self):
|
|
"""Test getting current theme when it exists."""
|
|
request = Mock(spec=Request)
|
|
test_theme = {"theme_name": "test"}
|
|
request.state.theme = test_theme
|
|
|
|
theme = get_current_theme(request)
|
|
|
|
assert theme == test_theme
|
|
|
|
def test_get_current_theme_default(self):
|
|
"""Test getting theme returns default when not set."""
|
|
request = Mock(spec=Request)
|
|
request.state = Mock(spec=[]) # No theme attribute
|
|
|
|
theme = get_current_theme(request)
|
|
|
|
assert theme["theme_name"] == "default"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestThemeEdgeCases:
|
|
"""Test suite for edge cases and special scenarios."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_middleware_closes_db_connection(self):
|
|
"""Test middleware properly closes database connection."""
|
|
middleware = ThemeContextMiddleware(app=None)
|
|
|
|
request = Mock(spec=Request)
|
|
mock_vendor = Mock(id=1, name="Test")
|
|
request.state = Mock(vendor=mock_vendor)
|
|
|
|
call_next = AsyncMock(return_value=Mock())
|
|
|
|
mock_db = MagicMock()
|
|
|
|
with patch("middleware.theme_context.get_db", return_value=iter([mock_db])):
|
|
await middleware.dispatch(request, call_next)
|
|
|
|
# Verify database was closed
|
|
mock_db.close.assert_called_once()
|
|
|
|
def test_theme_default_immutability(self):
|
|
"""Test that getting default theme doesn't share state."""
|
|
theme1 = ThemeContextManager.get_default_theme()
|
|
theme2 = ThemeContextManager.get_default_theme()
|
|
|
|
# Modify theme1
|
|
theme1["colors"]["primary"] = "#000000"
|
|
|
|
# theme2 should not be affected (if properly implemented)
|
|
# Note: This test documents expected behavior
|
|
assert theme2["colors"]["primary"] == "#6366f1"
|