Add comprehensive test cases for theme creation API
Implemented various tests to validate the theme creation process, including tests for successful creation, validation failures, image relocation, large data handling, and error scenarios such as database issues. Introduced mock dependencies to simulate behaviors like file relocation and database operations. Ensured all edge cases are covered to improve robustness and reliability.
This commit is contained in:
@@ -4,7 +4,9 @@ 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:
|
||||
@@ -35,3 +37,533 @@ class TestCreateEventTheme:
|
||||
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
|
||||
Reference in New Issue
Block a user