""" Unit Tests for Worksheet Editor API Tests cover: - Worksheet CRUD (create, read, list, delete) - AI Image generation (mocked) - PDF Export - Health check DSGVO Note: All tests run locally without external API calls. BACKLOG: Feature not yet integrated into main.py See: https://macmini:3002/infrastructure/tests """ import pytest import sys # Skip entire module if worksheet_editor_api is not available pytest.importorskip("worksheet_editor_api", reason="worksheet_editor_api not yet integrated - Backlog item") # Mark all tests in this module as expected failures (backlog item) pytestmark = pytest.mark.xfail( reason="worksheet_editor_api not yet integrated into main.py - Backlog item", strict=False # Don't fail if test unexpectedly passes ) import json import uuid import io import os from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch from fastapi.testclient import TestClient # Import the main app and worksheet-editor components sys.path.insert(0, '..') from main import app from worksheet_editor_api import ( worksheets_db, AIImageStyle, WorksheetStatus, WORKSHEET_STORAGE_DIR, ) # ============================================= # FIXTURES # ============================================= @pytest.fixture def client(): """Test client for FastAPI app.""" return TestClient(app) @pytest.fixture(autouse=True) def clear_storage(): """Clear in-memory storage before each test.""" worksheets_db.clear() # Clean up test files if os.path.exists(WORKSHEET_STORAGE_DIR): for f in os.listdir(WORKSHEET_STORAGE_DIR): if f.startswith('test_') or f.startswith('ws_test_'): os.remove(os.path.join(WORKSHEET_STORAGE_DIR, f)) yield worksheets_db.clear() @pytest.fixture def sample_worksheet_data(): """Sample worksheet data.""" return { "title": "Test Arbeitsblatt", "description": "Testbeschreibung", "pages": [ { "id": "page_1", "index": 0, "canvasJSON": json.dumps({ "objects": [ { "type": "text", "text": "Überschrift", "left": 100, "top": 50, "fontSize": 24 }, { "type": "rect", "left": 100, "top": 100, "width": 200, "height": 100 } ] }) } ], "pageFormat": { "width": 210, "height": 297, "orientation": "portrait", "margins": {"top": 15, "right": 15, "bottom": 15, "left": 15} } } @pytest.fixture def sample_multipage_worksheet(): """Sample worksheet with multiple pages.""" return { "title": "Mehrseitiges Arbeitsblatt", "pages": [ {"id": "page_1", "index": 0, "canvasJSON": "{}"}, {"id": "page_2", "index": 1, "canvasJSON": "{}"}, {"id": "page_3", "index": 2, "canvasJSON": "{}"}, ] } # ============================================= # WORKSHEET CRUD TESTS # ============================================= class TestWorksheetCRUD: """Tests for Worksheet CRUD operations.""" def test_create_worksheet(self, client, sample_worksheet_data): """Test creating a new worksheet.""" response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) assert response.status_code == 200 data = response.json() assert "id" in data assert data["id"].startswith("ws_") assert data["title"] == sample_worksheet_data["title"] assert data["description"] == sample_worksheet_data["description"] assert len(data["pages"]) == 1 assert "createdAt" in data assert "updatedAt" in data def test_update_worksheet(self, client, sample_worksheet_data): """Test updating an existing worksheet.""" # First create create_response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) worksheet_id = create_response.json()["id"] # Then update sample_worksheet_data["id"] = worksheet_id sample_worksheet_data["title"] = "Aktualisiertes Arbeitsblatt" update_response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) assert update_response.status_code == 200 data = update_response.json() assert data["id"] == worksheet_id assert data["title"] == "Aktualisiertes Arbeitsblatt" def test_get_worksheet(self, client, sample_worksheet_data): """Test retrieving a worksheet by ID.""" # Create first create_response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) worksheet_id = create_response.json()["id"] # Then get get_response = client.get(f"/api/v1/worksheet/{worksheet_id}") assert get_response.status_code == 200 data = get_response.json() assert data["id"] == worksheet_id assert data["title"] == sample_worksheet_data["title"] def test_get_nonexistent_worksheet(self, client): """Test retrieving a non-existent worksheet.""" response = client.get("/api/v1/worksheet/ws_nonexistent123") assert response.status_code == 404 assert "not found" in response.json()["detail"].lower() def test_list_worksheets(self, client, sample_worksheet_data): """Test listing all worksheets.""" # Create multiple worksheets for i in range(3): data = sample_worksheet_data.copy() data["title"] = f"Arbeitsblatt {i+1}" client.post("/api/v1/worksheet/save", json=data) # List all response = client.get("/api/v1/worksheet/list/all") assert response.status_code == 200 data = response.json() assert "worksheets" in data assert "total" in data assert data["total"] >= 3 def test_delete_worksheet(self, client, sample_worksheet_data): """Test deleting a worksheet.""" # Create first create_response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) worksheet_id = create_response.json()["id"] # Delete delete_response = client.delete(f"/api/v1/worksheet/{worksheet_id}") assert delete_response.status_code == 200 assert delete_response.json()["status"] == "deleted" # Verify it's gone get_response = client.get(f"/api/v1/worksheet/{worksheet_id}") assert get_response.status_code == 404 def test_delete_nonexistent_worksheet(self, client): """Test deleting a non-existent worksheet.""" response = client.delete("/api/v1/worksheet/ws_nonexistent123") assert response.status_code == 404 # ============================================= # MULTIPAGE TESTS # ============================================= class TestMultipageWorksheet: """Tests for multi-page worksheet functionality.""" def test_create_multipage_worksheet(self, client, sample_multipage_worksheet): """Test creating a worksheet with multiple pages.""" response = client.post( "/api/v1/worksheet/save", json=sample_multipage_worksheet ) assert response.status_code == 200 data = response.json() assert len(data["pages"]) == 3 def test_page_indices(self, client, sample_multipage_worksheet): """Test that page indices are preserved.""" response = client.post( "/api/v1/worksheet/save", json=sample_multipage_worksheet ) data = response.json() for i, page in enumerate(data["pages"]): assert page["index"] == i # ============================================= # AI IMAGE TESTS # ============================================= class TestAIImageGeneration: """Tests for AI image generation.""" def test_ai_image_with_valid_prompt(self, client): """Test AI image generation with valid prompt.""" response = client.post( "/api/v1/worksheet/ai-image", json={ "prompt": "A friendly cartoon dog reading a book", "style": "cartoon", "width": 256, "height": 256 } ) # Should return 200 (with placeholder) since Ollama may not be running assert response.status_code == 200 data = response.json() assert "image_base64" in data assert data["image_base64"].startswith("data:image/png;base64,") assert "prompt_used" in data def test_ai_image_styles(self, client): """Test different AI image styles.""" styles = ["realistic", "cartoon", "sketch", "clipart", "educational"] for style in styles: response = client.post( "/api/v1/worksheet/ai-image", json={ "prompt": "Test image", "style": style, "width": 256, "height": 256 } ) assert response.status_code == 200 data = response.json() assert style in data["prompt_used"].lower() or "test image" in data["prompt_used"].lower() def test_ai_image_empty_prompt(self, client): """Test AI image generation with empty prompt.""" response = client.post( "/api/v1/worksheet/ai-image", json={ "prompt": "", "style": "educational", "width": 256, "height": 256 } ) assert response.status_code == 422 # Validation error def test_ai_image_invalid_dimensions(self, client): """Test AI image generation with invalid dimensions.""" response = client.post( "/api/v1/worksheet/ai-image", json={ "prompt": "Test", "style": "educational", "width": 50, # Too small "height": 256 } ) assert response.status_code == 422 # Validation error # ============================================= # CANVAS JSON TESTS # ============================================= class TestCanvasJSON: """Tests for canvas JSON handling.""" def test_save_and_load_canvas_json(self, client): """Test that canvas JSON is preserved correctly.""" canvas_data = { "objects": [ {"type": "text", "text": "Hello", "left": 10, "top": 10}, {"type": "rect", "left": 50, "top": 50, "width": 100, "height": 50} ], "background": "#ffffff" } worksheet_data = { "title": "Canvas Test", "pages": [{ "id": "page_1", "index": 0, "canvasJSON": json.dumps(canvas_data) }] } # Save create_response = client.post( "/api/v1/worksheet/save", json=worksheet_data ) worksheet_id = create_response.json()["id"] # Load get_response = client.get(f"/api/v1/worksheet/{worksheet_id}") loaded_data = get_response.json() # Compare loaded_canvas = json.loads(loaded_data["pages"][0]["canvasJSON"]) assert loaded_canvas["objects"] == canvas_data["objects"] def test_empty_canvas_json(self, client): """Test worksheet with empty canvas JSON.""" worksheet_data = { "title": "Empty Canvas", "pages": [{ "id": "page_1", "index": 0, "canvasJSON": "{}" }] } response = client.post( "/api/v1/worksheet/save", json=worksheet_data ) assert response.status_code == 200 # ============================================= # PAGE FORMAT TESTS # ============================================= class TestPageFormat: """Tests for page format handling.""" def test_default_page_format(self, client): """Test that default page format is applied.""" worksheet_data = { "title": "Default Format", "pages": [{"id": "p1", "index": 0, "canvasJSON": "{}"}] } response = client.post( "/api/v1/worksheet/save", json=worksheet_data ) data = response.json() assert data["pageFormat"]["width"] == 210 assert data["pageFormat"]["height"] == 297 assert data["pageFormat"]["orientation"] == "portrait" def test_custom_page_format(self, client): """Test custom page format.""" worksheet_data = { "title": "Custom Format", "pages": [{"id": "p1", "index": 0, "canvasJSON": "{}"}], "pageFormat": { "width": 297, "height": 210, "orientation": "landscape", "margins": {"top": 20, "right": 20, "bottom": 20, "left": 20} } } response = client.post( "/api/v1/worksheet/save", json=worksheet_data ) data = response.json() assert data["pageFormat"]["width"] == 297 assert data["pageFormat"]["height"] == 210 assert data["pageFormat"]["orientation"] == "landscape" # ============================================= # HEALTH CHECK TESTS # ============================================= class TestHealthCheck: """Tests for health check endpoint.""" def test_health_check(self, client): """Test health check endpoint.""" response = client.get("/api/v1/worksheet/health/check") assert response.status_code == 200 data = response.json() assert "status" in data assert data["status"] == "healthy" assert "storage" in data assert "reportlab" in data # ============================================= # PDF EXPORT TESTS # ============================================= class TestPDFExport: """Tests for PDF export functionality.""" def test_export_pdf(self, client, sample_worksheet_data): """Test PDF export of a worksheet.""" # Create worksheet create_response = client.post( "/api/v1/worksheet/save", json=sample_worksheet_data ) worksheet_id = create_response.json()["id"] # Export export_response = client.post( f"/api/v1/worksheet/{worksheet_id}/export-pdf" ) # Check response (may be 501 if reportlab not installed) assert export_response.status_code in [200, 501] if export_response.status_code == 200: assert export_response.headers["content-type"] == "application/pdf" assert "attachment" in export_response.headers.get("content-disposition", "") def test_export_nonexistent_worksheet_pdf(self, client): """Test PDF export of non-existent worksheet.""" response = client.post("/api/v1/worksheet/ws_nonexistent/export-pdf") assert response.status_code in [404, 501] # ============================================= # ERROR HANDLING TESTS # ============================================= class TestErrorHandling: """Tests for error handling.""" def test_invalid_json(self, client): """Test handling of invalid JSON.""" response = client.post( "/api/v1/worksheet/save", content="not valid json", headers={"Content-Type": "application/json"} ) assert response.status_code == 422 def test_missing_required_fields(self, client): """Test handling of missing required fields.""" response = client.post( "/api/v1/worksheet/save", json={"pages": []} # Missing title ) assert response.status_code == 422 def test_empty_pages_array(self, client): """Test worksheet with empty pages array.""" response = client.post( "/api/v1/worksheet/save", json={ "title": "No Pages", "pages": [] } ) # Should still work - empty worksheets are allowed assert response.status_code == 200