# tests/api/routes/test_event_themes.py import uuid from fastapi import status import pytest from app.api.dependencies.common import get_storage_provider from app.api.routes.event_themes import router as themes_router from unittest.mock import patch, MagicMock class TestCreateEventTheme: """Test scenarios for the create_theme endpoint.""" @pytest.fixture(autouse=True) def setup_method(self, create_test_client, db_session, mock_superuser): self.client = create_test_client( router=themes_router, prefix="/themes", db_session=db_session, user=mock_superuser ) self.db_session = db_session self.mock_superuser = mock_superuser def test_create_theme_success(self, theme_data): """Test successful theme creation.""" # Make the request response = self.client.post("/themes/", json=theme_data) # Assert response assert response.status_code == 200 data = response.json() assert data["name"] == theme_data["name"] assert data["description"] == theme_data["description"] assert data["preview_image_url"] == theme_data["preview_image_url"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data def test_create_theme_unauthorized_user(self, create_test_client, db_session, theme_data): """Test that unauthenticated users cannot create themes.""" # Create a client with no authenticated user client = create_test_client( router=themes_router, prefix="/themes", db_session=db_session, user=None ) # Make the request response = client.post("/themes/", json=theme_data) # Assert unauthorized response assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Invalid authentication credentials" in response.json()["detail"] def test_create_theme_regular_user_forbidden(self, create_test_client, db_session, theme_data, mock_user): """Test that regular users (non-superusers) cannot create themes.""" # Create a client with a regular user (not superuser) client = create_test_client( router=themes_router, prefix="/themes", db_session=db_session, user=mock_user ) # Make the request response = client.post("/themes/", json=theme_data) # Assert forbidden response assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Invalid authentication credentials" in response.json()["detail"] def test_create_theme_with_minimal_data(self): """Test creating a theme with only the required minimal fields.""" # Create minimal theme data with only required fields minimal_theme_data = { "name": "Minimal Theme", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=minimal_theme_data) # Assert successful response assert response.status_code == status.HTTP_200_OK data = response.json() assert data["name"] == minimal_theme_data["name"] assert data["color_palette"] == minimal_theme_data["color_palette"] assert data["fonts"] == minimal_theme_data["fonts"] assert "id" in data # Verify default values are set correctly assert data["is_active"] == True assert data["description"] is None assert data["preview_image_url"] is None assert data["background_image_url"] is None assert data["foreground_image_url"] is None assert data["asset_image_urls"] is None def test_create_theme_empty_name_fails(self): """Test that creating a theme with an empty name fails validation.""" # Theme data with empty name invalid_data = { "name": "", # Empty name should fail validation "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=invalid_data) # Assert validation error assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY errors = response.json()["detail"] assert any("name" in error["loc"] for error in errors) def test_create_theme_empty_color_palette_fails(self): """Test that creating a theme with an empty color palette fails validation.""" # Theme data with empty color palette invalid_data = { "name": "Invalid Theme", "color_palette": {}, # Empty color palette should fail validation "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=invalid_data) # Assert validation error assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY errors = response.json()["detail"] assert any("color_palette" in error["loc"] for error in errors) def test_create_theme_empty_fonts_fails(self): """Test that creating a theme with empty fonts fails validation.""" # Theme data with empty fonts invalid_data = { "name": "Invalid Theme", "color_palette": { "primary": "#000000" }, "fonts": {} # Empty fonts should fail validation } # Make the request response = self.client.post("/themes/", json=invalid_data) # Assert validation error assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY errors = response.json()["detail"] assert any("fonts" in error["loc"] for error in errors) def test_create_theme_with_uploaded_preview_image(self, db_session): """Test creating a theme with an uploaded preview image that needs relocation.""" # Path to where the _relocate_theme_file is imported and used @patch('app.api.routes.event_themes._relocate_theme_file') def run_test(mock_relocate): # Configure the mock to return a fixed URL mock_relocated_url = "/files/event-themes/mock-id/preview/relocated-image.jpg" mock_relocate.return_value = mock_relocated_url # Create test data with URL that matches the expected pattern theme_data = { "name": "Theme with Preview Image", "preview_image_url": "/files/uploads/temp-image.jpg", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response assert response.status_code == 200 # Assert the mock was called mock_relocate.assert_called_once() # Get the response data data = response.json() # Assert the URL was updated in the response assert data["preview_image_url"] == mock_relocated_url # Verify other theme data assert data["name"] == theme_data["name"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data # Verify the call parameters (theme_id, url, file_type, storage) call_args = mock_relocate.call_args[0] assert isinstance(call_args[0], uuid.UUID) # theme_id assert call_args[1] == theme_data["preview_image_url"] # url assert call_args[2] == 'preview' # file_type return data return run_test() def test_create_theme_with_uploaded_background_image(self, db_session): """Test creating a theme with an uploaded background image that needs relocation.""" # Path to where the _relocate_theme_file is imported and used @patch('app.api.routes.event_themes._relocate_theme_file') def run_test(mock_relocate): # Configure the mock to return a fixed URL mock_relocated_url = "/files/event-themes/mock-id/background/relocated-bg.jpg" mock_relocate.return_value = mock_relocated_url # Create test data with URL that includes a background image theme_data = { "name": "Theme with Background Image", "background_image_url": "/files/uploads/background.jpg", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response assert response.status_code == 200 # Assert the mock was called mock_relocate.assert_called_once() # Get the response data data = response.json() # Assert the URL was updated in the response assert data["background_image_url"] == mock_relocated_url # Verify other theme data assert data["name"] == theme_data["name"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data # Verify the call parameters (theme_id, url, file_type, storage) call_args = mock_relocate.call_args[0] assert isinstance(call_args[0], uuid.UUID) # theme_id assert call_args[1] == theme_data["background_image_url"] # url assert call_args[2] == 'background' # file_type return data # Run the test return run_test() def test_create_theme_with_uploaded_foreground_image(self, db_session): """Test creating a theme with an uploaded foreground image that needs relocation.""" # Path to where the _relocate_theme_file is imported and used @patch('app.api.routes.event_themes._relocate_theme_file') def run_test(mock_relocate): # Configure the mock to return a fixed URL mock_relocated_url = "/files/event-themes/mock-id/foreground/relocated-fg.jpg" mock_relocate.return_value = mock_relocated_url # Create test data with URL that includes a foreground image theme_data = { "name": "Theme with Foreground Image", "foreground_image_url": "/files/uploads/foreground.jpg", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response assert response.status_code == 200 # Assert the mock was called mock_relocate.assert_called_once() # Get the response data data = response.json() # Assert the URL was updated in the response assert data["foreground_image_url"] == mock_relocated_url # Verify other theme data assert data["name"] == theme_data["name"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data # Verify the call parameters (theme_id, url, file_type, storage) call_args = mock_relocate.call_args[0] assert isinstance(call_args[0], uuid.UUID) # theme_id assert call_args[1] == theme_data["foreground_image_url"] # url assert call_args[2] == 'foreground' # file_type return data # Run the test return run_test() def test_create_theme_with_uploaded_asset_images(self, db_session): """Test creating a theme with uploaded asset images that need relocation.""" # Path to where the _relocate_theme_file is imported and used @patch('app.api.routes.event_themes._relocate_theme_file') def run_test(mock_relocate): # Configure the mock to return different URLs based on the asset key def side_effect(theme_id, current_url, file_type, storage): # Extract the asset key from the file_type path (format: 'assets/{key}') asset_key = file_type.split('/')[-1] if '/' in file_type else '' return f"/files/event-themes/mock-id/assets/{asset_key}/relocated-asset.jpg" mock_relocate.side_effect = side_effect # Create test data with asset image URLs theme_data = { "name": "Theme with Asset Images", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" }, "asset_image_urls": { "logo": "/files/uploads/logo.jpg", "icon": "/files/uploads/icon.png", "banner": "/files/uploads/banner.jpg" } } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response assert response.status_code == 200 # Assert the mock was called multiple times (once for each asset) assert mock_relocate.call_count == 3 # Get the response data data = response.json() # Verify the asset URLs were updated in the response assert "asset_image_urls" in data assert data["asset_image_urls"]["logo"] == "/files/event-themes/mock-id/assets/logo/relocated-asset.jpg" assert data["asset_image_urls"]["icon"] == "/files/event-themes/mock-id/assets/icon/relocated-asset.jpg" assert data["asset_image_urls"]["banner"] == "/files/event-themes/mock-id/assets/banner/relocated-asset.jpg" # Verify other theme data assert data["name"] == theme_data["name"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data # Verify the mock was called with the correct parameters for each asset calls = mock_relocate.call_args_list call_urls = [call.args[1] for call in calls] call_types = [call.args[2] for call in calls] # Check that all expected URLs were processed for key, url in theme_data["asset_image_urls"].items(): assert url in call_urls assert f"assets/{key}" in call_types return data # Run the test return run_test() def test_create_theme_image_relocation_failure_handled(self, db_session): """Test that theme creation handles image relocation failures gracefully.""" # Path to where the _relocate_theme_file is imported and used @patch('app.api.routes.event_themes._relocate_theme_file') def run_test(mock_relocate): # Configure the mock to return None, simulating a relocation failure mock_relocate.return_value = None # Create test data with an image URL that should trigger relocation theme_data = { "name": "Theme with Failed Image Relocation", "preview_image_url": "/files/uploads/image-that-fails.jpg", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response (theme should still be created even if relocation fails) assert response.status_code == 200 # Assert the mock was called mock_relocate.assert_called_once() # Get the response data data = response.json() # Verify the original URL was kept (since relocation failed) assert data["preview_image_url"] == theme_data["preview_image_url"] # Verify other theme data assert data["name"] == theme_data["name"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert "id" in data # Verify the call parameters call_args = mock_relocate.call_args[0] assert isinstance(call_args[0], uuid.UUID) # theme_id assert call_args[1] == theme_data["preview_image_url"] # url assert call_args[2] == 'preview' # file_type return data # Run the test return run_test() def test_create_theme_database_error_handled(self, db_session): """Test that theme creation attempts to create the theme but fails correctly.""" @patch('app.crud.event_theme.event_theme.create') def run_test(mock_create): # Configure the mock to raise a SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError mock_create.side_effect = SQLAlchemyError("Database error") # Create test data for a new theme theme_data = { "name": "Theme That Will Fail", "color_palette": { "primary": "#000000" }, "fonts": { "main": "Arial" } } # Make the request and expect an exception try: response = self.client.post("/themes/", json=theme_data) assert False, "Expected request to fail with SQLAlchemyError" except SQLAlchemyError: pass # Expected error # Assert the mock was called mock_create.assert_called_once() # Verify the call parameters include our theme data call_kwargs = mock_create.call_args.kwargs assert "obj_in" in call_kwargs return True # Test passed if we get here # Run the test return run_test() def test_create_theme_with_large_data(self, db_session): """Test that themes with substantial amounts of data can be created successfully.""" # Create a moderately large description (1000 chars) large_description = "A detailed theme description. " * 50 # ~1000 characters # A color palette with more entries than usual large_color_palette = { "primary": "#ff5722", "secondary": "#4caf50", "background": "#ffffff", "text": "#000000", "accent1": "#2196f3", "accent2": "#f44336", "accent3": "#9c27b0", "accent4": "#ffeb3b", "accent5": "#795548", "accent6": "#607d8b", "highlight": "#03a9f4", "subtle": "#e0e0e0", "success": "#4caf50", "warning": "#ff9800", "error": "#f44336", "info": "#2196f3", "dark": "#212121", "light": "#f5f5f5", "border": "#e0e0e0", "shadow": "#757575" } # Multiple font specifications large_fonts = { "header": "Roboto, sans-serif", "subheader": "Montserrat, sans-serif", "body": "Open Sans, sans-serif", "button": "Roboto Condensed, sans-serif", "caption": "Roboto, sans-serif", "code": "Source Code Pro, monospace", "emphasis": "Georgia, serif", "quote": "Playfair Display, serif", "small": "Roboto, sans-serif", "display": "Montserrat, sans-serif" } # Asset images for various theme elements asset_images = { "header_bg": "/files/uploads/header_bg.jpg", "footer_bg": "/files/uploads/footer_bg.jpg", "sidebar_bg": "/files/uploads/sidebar_bg.jpg", "hero_image": "/files/uploads/hero.jpg", "logo_light": "/files/uploads/logo_light.png", "logo_dark": "/files/uploads/logo_dark.png", "pattern_light": "/files/uploads/pattern_light.png", "pattern_dark": "/files/uploads/pattern_dark.png", "icon_set": "/files/uploads/icons.svg", "background_texture": "/files/uploads/texture.jpg" } # Theme data with large content theme_data = { "name": "Theme with Large Data", "description": large_description, "color_palette": large_color_palette, "fonts": large_fonts, "asset_image_urls": asset_images } # Make the request response = self.client.post("/themes/", json=theme_data) # Assert successful response assert response.status_code == 200 # Get the response data data = response.json() # Verify the large data was properly saved assert data["name"] == theme_data["name"] assert data["description"] == theme_data["description"] assert data["color_palette"] == theme_data["color_palette"] assert data["fonts"] == theme_data["fonts"] assert data["asset_image_urls"] == theme_data["asset_image_urls"] assert "id" in data # Verify the theme was actually created in the database from app.models.event_theme import EventTheme created_id = uuid.UUID(data["id"]) db_theme = db_session.query(EventTheme).filter(EventTheme.id == created_id).first() assert db_theme is not None assert db_theme.name == theme_data["name"] return data