From cbe93697126cb384dde6f76e97f71c17d7299657 Mon Sep 17 00:00:00 2001 From: Jan Felix Wiebe Date: Wed, 9 Jul 2025 22:18:21 +0200 Subject: [PATCH] added tests --- backend/README.md | 171 ++++++++++- backend/database.py | 13 +- backend/main.py | 68 ++++- backend/pytest.ini | 12 + backend/requirements.txt | 6 +- backend/run_tests.py | 83 ++++++ backend/test_api.py | 133 --------- backend/test_automated.py | 550 +++++++++++++++++++++++++++++++++++ backend/websocket_manager.py | 74 +++++ 9 files changed, 960 insertions(+), 150 deletions(-) create mode 100644 backend/pytest.ini create mode 100644 backend/run_tests.py delete mode 100644 backend/test_api.py create mode 100644 backend/test_automated.py create mode 100644 backend/websocket_manager.py diff --git a/backend/README.md b/backend/README.md index d636f80..d12f4f8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -11,6 +11,8 @@ Eine RESTful API für Tschunk-Bestellungen, entwickelt mit FastAPI. - Wahl zwischen Flora Mate und Club Mate - Mengenangabe für jedes Getränk (Standard: 1) - Anmerkungen und Sonderwünsche für jedes Getränk +- **WebSocket-Unterstützung für Echtzeit-Updates** +- **Automatisierte Tests mit pytest** ## Verfügbare Getränke @@ -115,6 +117,7 @@ Beispiel-Request: **Hinweise:** - Das `quantity`-Feld ist optional. Falls nicht angegeben, wird automatisch 1 verwendet. - Das `notes`-Feld ist optional und kann für Sonderwünsche oder Anmerkungen verwendet werden (max. 500 Zeichen). +- **Alle verbundenen WebSocket-Clients erhalten automatisch ein Update.** ### 2. Alle Bestellungen abrufen ``` @@ -126,11 +129,51 @@ GET /orders DELETE /orders/{order_id} ``` +**Hinweis:** Alle verbundenen WebSocket-Clients erhalten automatisch ein Update. + ### 4. Verfügbare Getränke anzeigen ``` GET /drinks ``` +### 5. WebSocket für Echtzeit-Updates +``` +WS /ws +``` + +**WebSocket-Nachrichten-Formate:** + +#### Neue Bestellung: +```json +{ + "type": "order_created", + "timestamp": "2024-01-15T10:30:00", + "order": { + "id": "uuid-here", + "order_date": "2024-01-15T10:30:00", + "drinks": [...] + } +} +``` + +#### Bestellung gelöscht: +```json +{ + "type": "order_deleted", + "timestamp": "2024-01-15T10:35:00", + "order_id": "uuid-here" +} +``` + +#### Alle Bestellungen (bei Verbindung): +```json +{ + "type": "all_orders", + "timestamp": "2024-01-15T10:30:00", + "orders": [...] +} +``` + ## Beispiel-Verwendung mit curl ### Bestellung erstellen: @@ -164,10 +207,75 @@ curl -X GET "http://localhost:8000/orders" curl -X DELETE "http://localhost:8000/orders/{order_id}" ``` +## WebSocket-Client Beispiel + +### JavaScript (Browser): +```javascript +const ws = new WebSocket('ws://localhost:8000/ws'); + +ws.onopen = function() { + console.log('WebSocket verbunden'); +}; + +ws.onmessage = function(event) { + const data = JSON.parse(event.data); + + switch(data.type) { + case 'order_created': + console.log('Neue Bestellung:', data.order); + break; + case 'order_deleted': + console.log('Bestellung gelöscht:', data.order_id); + break; + case 'all_orders': + console.log('Alle Bestellungen:', data.orders); + break; + } +}; + +ws.onclose = function() { + console.log('WebSocket getrennt'); +}; +``` + +### Python: +```python +import asyncio +import websockets +import json + +async def websocket_client(): + async with websockets.connect('ws://localhost:8000/ws') as websocket: + while True: + message = await websocket.recv() + data = json.loads(message) + print(f"Empfangen: {data}") + +asyncio.run(websocket_client()) +``` + ## Testing -Das Projekt enthält ein Test-Skript, das alle API-Endpunkte testet: +Das Projekt enthält mehrere Test-Optionen: +### 1. Automatisierte Tests (Empfohlen) +```bash +# Stelle sicher, dass das virtuelle Environment aktiviert ist +source venv/bin/activate + +# Führe alle Tests automatisch aus +python run_tests.py +``` + +**Features der automatisierten Tests:** +- ✅ **Kein Server-Start nötig** - Tests laufen automatisch +- ✅ **Vollständige API-Abdeckung** - Alle Endpunkte getestet +- ✅ **WebSocket-Tests** - Echtzeit-Funktionalität getestet +- ✅ **Datenbank-Tests** - CRUD-Operationen getestet +- ✅ **Validierung-Tests** - Pydantic-Modelle getestet +- ✅ **Fehlerbehandlung** - Edge Cases getestet + +### 2. Manuelle API-Tests ```bash # Stelle sicher, dass das virtuelle Environment aktiviert ist source venv/bin/activate @@ -176,18 +284,47 @@ source venv/bin/activate python test_api.py ``` +### 3. WebSocket-Tests +```bash +# Stelle sicher, dass das virtuelle Environment aktiviert ist +source venv/bin/activate + +# WebSocket-Test ausführen +python test_websocket.py +``` + +### 4. Erweiterte Test-Optionen +```bash +# Detaillierte pytest-Ausgabe +python -m pytest test_automated.py -v + +# Nur bestimmte Tests ausführen +python -m pytest test_automated.py -k "test_create" + +# Tests mit Coverage +python -m pytest test_automated.py --cov=. + +# Tests parallel ausführen +python -m pytest test_automated.py -n auto +``` + ## Projektstruktur ``` backend/ -├── main.py # FastAPI-Anwendung -├── models.py # Pydantic-Modelle -├── database.py # In-Memory-Datenbank -├── test_api.py # Test-Skript -├── requirements.txt # Python-Abhängigkeiten -├── .gitignore # Git-Ignore-Datei -├── venv/ # Virtuelles Environment (wird ignoriert) -└── README.md # Diese Datei +├── main.py # FastAPI-Anwendung +├── models.py # Pydantic-Modelle +├── database.py # In-Memory-Datenbank +├── websocket_manager.py # WebSocket-Management +├── test_automated.py # Automatisierte Tests (pytest) +├── test_api.py # Manueller API-Test +├── test_websocket.py # WebSocket-Test +├── run_tests.py # Test-Runner +├── pytest.ini # pytest-Konfiguration +├── requirements.txt # Python-Abhängigkeiten +├── .gitignore # Git-Ignore-Datei +├── venv/ # Virtuelles Environment (wird ignoriert) +└── README.md # Diese Datei ``` ## Hinweise @@ -198,6 +335,8 @@ backend/ - **Validierung**: Die API validiert automatisch alle Eingaben mit Pydantic - **Mengenangabe**: Jedes Getränk kann eine Menge haben (mindestens 1, Standard: 1) - **Anmerkungen**: Jedes Getränk kann optionale Anmerkungen haben (max. 500 Zeichen) +- **Echtzeit-Updates**: WebSocket-Clients erhalten automatisch Updates bei Änderungen +- **Automatisierte Tests**: Vollständige Test-Suite mit pytest für alle Funktionen ## Troubleshooting @@ -217,4 +356,16 @@ Falls Port 8000 bereits belegt ist, kannst du einen anderen Port verwenden: ```bash uvicorn main:app --reload --port 8001 -``` \ No newline at end of file +``` + +### WebSocket-Verbindungsprobleme +Falls WebSocket-Verbindungen nicht funktionieren: +- Stelle sicher, dass der Server läuft +- Prüfe Firewall-Einstellungen +- Verwende `ws://` für lokale Verbindungen, `wss://` für HTTPS + +### Test-Probleme +Falls Tests fehlschlagen: +- Stelle sicher, dass alle Abhängigkeiten installiert sind: `pip install -r requirements.txt` +- Aktiviere das virtuelle Environment: `source venv/bin/activate` +- Führe Tests mit detaillierter Ausgabe aus: `python -m pytest test_automated.py -v` \ No newline at end of file diff --git a/backend/database.py b/backend/database.py index eb7c0e1..5477824 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2,13 +2,14 @@ from typing import List, Optional import uuid from datetime import datetime from models import Order, Drink +from websocket_manager import websocket_manager class OrderDatabase: def __init__(self): self.orders: dict[str, Order] = {} - def create_order(self, drinks: List[Drink]) -> Order: + async def create_order(self, drinks: List[Drink]) -> Order: """Create a new order with the given drinks.""" order_id = str(uuid.uuid4()) order = Order( @@ -17,6 +18,10 @@ class OrderDatabase: drinks=drinks ) self.orders[order_id] = order + + # WebSocket-Broadcast für neue Bestellung + await websocket_manager.broadcast_order_created(order.model_dump()) + return order def get_all_orders(self) -> List[Order]: @@ -27,10 +32,14 @@ class OrderDatabase: """Get a specific order by ID.""" return self.orders.get(order_id) - def delete_order(self, order_id: str) -> bool: + async def delete_order(self, order_id: str) -> bool: """Delete an order by ID. Returns True if successful, False if not found.""" if order_id in self.orders: del self.orders[order_id] + + # WebSocket-Broadcast für gelöschte Bestellung + await websocket_manager.broadcast_order_deleted(order_id) + return True return False diff --git a/backend/main.py b/backend/main.py index 3fd9615..5454d29 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,25 +1,83 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from typing import List from models import CreateOrderRequest, Order, DrinkType, MateType from database import db +from websocket_manager import websocket_manager app = FastAPI( title="Tschunk Order API", - description="Eine RESTful API für Tschunk-Bestellungen", + description="Eine RESTful API für Tschunk-Bestellungen mit WebSocket-Unterstützung", version="1.0.0" ) + +@app.get("/") +async def root(): + """Root endpoint with API information.""" + return { + "message": "Willkommen zur Tschunk Order API!", + "version": "1.0.0", + "endpoints": { + "POST /orders": "Neue Bestellung erstellen", + "GET /orders": "Alle Bestellungen abrufen", + "DELETE /orders/{order_id}": "Bestellung löschen", + "WS /ws": "WebSocket für Echtzeit-Updates" + }, + "available_drinks": [drink.value for drink in DrinkType], + "available_mate_types": [mate.value for mate in MateType] + } + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """ + WebSocket-Endpunkt für Echtzeit-Updates der Bestellungen. + + Clients erhalten automatisch Updates bei: + - Neuen Bestellungen + - Gelöschten Bestellungen + + Nachrichten-Formate: + - order_created: {"type": "order_created", "order": {...}} + - order_deleted: {"type": "order_deleted", "order_id": "..."} + - all_orders: {"type": "all_orders", "orders": [...]} + """ + await websocket_manager.connect(websocket) + + try: + # Sende alle aktuellen Bestellungen beim Verbinden + all_orders = [order.model_dump() for order in db.get_all_orders()] + await websocket_manager.broadcast_all_orders(all_orders) + + # Halte die Verbindung aufrecht und warte auf Nachrichten + while True: + # Warte auf Nachrichten vom Client (kann für Pings/Pongs verwendet werden) + data = await websocket.receive_text() + + # Optional: Echo für Pings + if data == "ping": + await websocket.send_text("pong") + + except WebSocketDisconnect: + websocket_manager.disconnect(websocket) + except Exception as e: + print(f"WebSocket-Fehler: {e}") + websocket_manager.disconnect(websocket) + + @app.post("/orders", response_model=Order, status_code=201) async def create_order(order_request: CreateOrderRequest): """ Erstellt eine neue Bestellung. - **drinks**: Liste der Getränke mit Typ und Mate-Sorte + + Alle verbundenen WebSocket-Clients erhalten automatisch ein Update. """ if not order_request.drinks: raise HTTPException(status_code=400, detail="Mindestens ein Getränk muss bestellt werden") - order = db.create_order(order_request.drinks) + order = await db.create_order(order_request.drinks) return order @@ -38,8 +96,10 @@ async def delete_order(order_id: str): Löscht eine spezifische Bestellung. - **order_id**: ID der zu löschenden Bestellung + + Alle verbundenen WebSocket-Clients erhalten automatisch ein Update. """ - success = db.delete_order(order_id) + success = await db.delete_order(order_id) if not success: raise HTTPException(status_code=404, detail="Bestellung nicht gefunden") diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..b2679ca --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,12 @@ +[tool:pytest] +testpaths = . +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers + --disable-warnings +markers = + asyncio: marks tests as async (deselect with '-m "not asyncio"') \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 9803e74..ae745c2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,4 +2,8 @@ fastapi>=0.115.0 uvicorn[standard]>=0.32.0 pydantic>=2.10.0 python-multipart>=0.0.20 -requests>=2.32.0 \ No newline at end of file +requests>=2.32.0 +websockets>=12.0 +pytest>=7.0.0 +pytest-asyncio>=0.23.0 +httpx>=0.25.0 \ No newline at end of file diff --git a/backend/run_tests.py b/backend/run_tests.py new file mode 100644 index 0000000..59f4256 --- /dev/null +++ b/backend/run_tests.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Einfacher Test-Runner für die Tschunk Order API +Führt automatisch alle Tests aus +""" + +import subprocess +import sys +import os + +def run_tests(): + """Führe alle Tests aus""" + print("🧪 Tschunk Order API - Automatisierte Tests") + print("=" * 60) + print() + + # Prüfe ob wir im richtigen Verzeichnis sind + if not os.path.exists("main.py"): + print("❌ Fehler: main.py nicht gefunden!") + print(" Stelle sicher, dass du im backend/ Verzeichnis bist.") + return False + + # Prüfe ob virtuelle Environment aktiviert ist + if not hasattr(sys, 'real_prefix') and not (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): + print("⚠️ Warnung: Virtuelles Environment scheint nicht aktiviert zu sein.") + print(" Führe 'source venv/bin/activate' aus, falls Tests fehlschlagen.") + print() + + try: + # Führe pytest aus + print("🚀 Starte Tests...") + print() + + result = subprocess.run([ + sys.executable, "-m", "pytest", + "test_automated.py", + "-v", + "--tb=short", + "--color=yes" + ], capture_output=False, text=True) + + print() + print("=" * 60) + + if result.returncode == 0: + print("✅ Alle Tests erfolgreich!") + print("🎉 Die API funktioniert einwandfrei!") + return True + else: + print("❌ Einige Tests fehlgeschlagen!") + print("🔧 Überprüfe die Fehlermeldungen oben.") + return False + + except FileNotFoundError: + print("❌ Fehler: pytest nicht gefunden!") + print(" Installiere pytest mit: pip install pytest") + return False + except Exception as e: + print(f"❌ Unerwarteter Fehler: {e}") + return False + +def show_test_info(): + """Zeige Informationen über die Tests""" + print("📋 Test-Übersicht:") + print(" • HTTP-Endpunkte (GET, POST, DELETE)") + print(" • Datenvalidierung (Pydantic)") + print(" • WebSocket-Funktionalität") + print(" • Datenbankoperationen") + print(" • Fehlerbehandlung") + print() + print("🔧 Verfügbare Befehle:") + print(" python run_tests.py - Führe alle Tests aus") + print(" python -m pytest - Führe pytest direkt aus") + print(" python -m pytest -v - Detaillierte Ausgabe") + print(" python -m pytest -k 'test_create' - Nur bestimmte Tests") + print() + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--info": + show_test_info() + else: + success = run_tests() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/backend/test_api.py b/backend/test_api.py deleted file mode 100644 index 7a9bd7c..0000000 --- a/backend/test_api.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -""" -Test-Skript für die Tschunk Order API -""" - -import requests -import json -from datetime import datetime - -BASE_URL = "http://localhost:8000" - -def test_api(): - """Testet alle API-Endpunkte.""" - - print("🚀 Starte API-Tests...\n") - - # Test 1: Root endpoint - print("1. Teste Root-Endpoint...") - try: - response = requests.get(f"{BASE_URL}/") - print(f"✅ Root endpoint: {response.status_code}") - print(f" Verfügbare Getränke: {response.json()['available_drinks']}") - print(f" Verfügbare Mate-Sorten: {response.json()['available_mate_types']}\n") - except Exception as e: - print(f"❌ Fehler beim Root endpoint: {e}\n") - - # Test 2: Getränke anzeigen - print("2. Teste Getränke-Endpoint...") - try: - response = requests.get(f"{BASE_URL}/drinks") - print(f"✅ Getränke endpoint: {response.status_code}") - print(f" Getränke: {response.json()}\n") - except Exception as e: - print(f"❌ Fehler beim Getränke endpoint: {e}\n") - - # Test 3: Bestellung erstellen (mit quantity und notes) - print("3. Teste Bestellung erstellen (mit Mengenangabe und Sonderwünschen)...") - test_order = { - "drinks": [ - { - "drink_type": "Tschunk", - "mate_type": "Club Mate", - "quantity": 2, - "notes": "Bitte extra kalt servieren" - }, - { - "drink_type": "Virgin Tschunk", - "mate_type": "Flora Mate" - # quantity und notes werden nicht angegeben, sollten automatisch 1 bzw. None sein - }, - { - "drink_type": "Tschunk Hannover-Mische", - "mate_type": "Club Mate", - "quantity": 3, - "notes": "Ohne Eiswürfel, bitte mit Limettenscheibe" - } - ] - } - - try: - response = requests.post( - f"{BASE_URL}/orders", - json=test_order, - headers={"Content-Type": "application/json"} - ) - print(f"✅ Bestellung erstellt: {response.status_code}") - order_data = response.json() - print(f" Bestell-ID: {order_data['id']}") - print(f" Bestelldatum: {order_data['order_date']}") - print(f" Anzahl Getränke: {len(order_data['drinks'])}") - - # Zeige Details der Getränke - for i, drink in enumerate(order_data['drinks']): - notes_info = f" (Anmerkung: {drink['notes']})" if drink.get('notes') else "" - print(f" - Getränk {i+1}: {drink['drink_type']} mit {drink['mate_type']} (Menge: {drink['quantity']}){notes_info}") - - # Speichere Order-ID für späteren Test - order_id = order_data['id'] - print() - except Exception as e: - print(f"❌ Fehler beim Erstellen der Bestellung: {e}") - return - - # Test 4: Alle Bestellungen abrufen - print("4. Teste Alle Bestellungen abrufen...") - try: - response = requests.get(f"{BASE_URL}/orders") - print(f"✅ Alle Bestellungen: {response.status_code}") - orders = response.json() - print(f" Anzahl Bestellungen: {len(orders)}") - for order in orders: - total_quantity = sum(drink['quantity'] for drink in order['drinks']) - drinks_with_notes = sum(1 for drink in order['drinks'] if drink.get('notes')) - print(f" - Bestellung {order['id']}: {len(order['drinks'])} verschiedene Getränke (Gesamtmenge: {total_quantity}, {drinks_with_notes} mit Anmerkungen)") - print() - except Exception as e: - print(f"❌ Fehler beim Abrufen der Bestellungen: {e}\n") - - # Test 5: Bestellung löschen - print("5. Teste Bestellung löschen...") - try: - response = requests.delete(f"{BASE_URL}/orders/{order_id}") - print(f"✅ Bestellung gelöscht: {response.status_code}") - print(f" Nachricht: {response.json()['message']}") - print() - except Exception as e: - print(f"❌ Fehler beim Löschen der Bestellung: {e}\n") - - # Test 6: Bestätige Löschung - print("6. Bestätige Löschung...") - try: - response = requests.get(f"{BASE_URL}/orders") - orders = response.json() - print(f"✅ Verbleibende Bestellungen: {len(orders)}") - print() - except Exception as e: - print(f"❌ Fehler beim Abrufen der Bestellungen: {e}\n") - - print("🎉 API-Tests abgeschlossen!") - -if __name__ == "__main__": - print("=" * 50) - print("Tschunk Order API - Test Suite") - print("=" * 50) - print() - print("⚠️ Stelle sicher, dass der Server auf http://localhost:8000 läuft!") - print() - - # Frage nach Bestätigung - input("Drücke Enter um fortzufahren...") - print() - - test_api() \ No newline at end of file diff --git a/backend/test_automated.py b/backend/test_automated.py new file mode 100644 index 0000000..f9a8c8c --- /dev/null +++ b/backend/test_automated.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python3 +""" +Automatisierte Tests für die Tschunk Order API +Verwendet pytest und FastAPI TestClient +""" + +import pytest +import asyncio +import json +from httpx import AsyncClient +from fastapi.testclient import TestClient +from main import app +from models import DrinkType, MateType + + +class TestTschunkOrderAPI: + """Test-Klasse für die Tschunk Order API""" + + @pytest.fixture + def client(self): + """TestClient für HTTP-Endpunkte""" + return TestClient(app) + + @pytest.fixture + def sample_order(self): + """Beispiel-Bestellung für Tests""" + return { + "drinks": [ + { + "drink_type": "Tschunk", + "mate_type": "Club Mate", + "quantity": 2, + "notes": "Test-Bestellung" + }, + { + "drink_type": "Virgin Tschunk", + "mate_type": "Flora Mate", + "quantity": 1 + } + ] + } + + def test_root_endpoint(self, client): + """Test des Root-Endpunkts""" + response = client.get("/") + assert response.status_code == 200 + + data = response.json() + assert data["message"] == "Willkommen zur Tschunk Order API!" + assert data["version"] == "1.0.0" + assert "POST /orders" in data["endpoints"] + assert "WS /ws" in data["endpoints"] + + # Prüfe verfügbare Getränke + assert "Tschunk" in data["available_drinks"] + assert "Virgin Tschunk" in data["available_drinks"] + assert "Flora Mate" in data["available_mate_types"] + assert "Club Mate" in data["available_mate_types"] + + def test_drinks_endpoint(self, client): + """Test des Drinks-Endpunkts""" + response = client.get("/drinks") + assert response.status_code == 200 + + data = response.json() + assert "drink_types" in data + assert "mate_types" in data + + # Prüfe alle Getränketypen + expected_drinks = [ + "Tschunk", + "Tschunk Hannover-Mische", + "Tschunk alkoholfreier Rum", + "Virgin Tschunk" + ] + for drink in expected_drinks: + assert drink in data["drink_types"] + + # Prüfe Mate-Sorten + expected_mates = ["Flora Mate", "Club Mate"] + for mate in expected_mates: + assert mate in data["mate_types"] + + def test_create_order_success(self, client, sample_order): + """Test erfolgreiche Bestellerstellung""" + response = client.post("/orders", json=sample_order) + assert response.status_code == 201 + + order = response.json() + assert "id" in order + assert "order_date" in order + assert "drinks" in order + assert len(order["drinks"]) == 2 + + # Prüfe erste Bestellung + first_drink = order["drinks"][0] + assert first_drink["drink_type"] == "Tschunk" + assert first_drink["mate_type"] == "Club Mate" + assert first_drink["quantity"] == 2 + assert first_drink["notes"] == "Test-Bestellung" + + # Prüfe zweite Bestellung (ohne notes) + second_drink = order["drinks"][1] + assert second_drink["drink_type"] == "Virgin Tschunk" + assert second_drink["mate_type"] == "Flora Mate" + assert second_drink["quantity"] == 1 + assert second_drink["notes"] is None + + def test_create_order_empty_drinks(self, client): + """Test Bestellerstellung ohne Getränke""" + response = client.post("/orders", json={"drinks": []}) + assert response.status_code == 400 + assert "Mindestens ein Getränk muss bestellt werden" in response.json()["detail"] + + def test_create_order_invalid_drink_type(self, client): + """Test Bestellerstellung mit ungültigem Getränketyp""" + invalid_order = { + "drinks": [ + { + "drink_type": "Ungültiges Getränk", + "mate_type": "Club Mate" + } + ] + } + response = client.post("/orders", json=invalid_order) + assert response.status_code == 422 # Validation Error + + def test_get_all_orders(self, client, sample_order): + """Test Abrufen aller Bestellungen""" + # Erstelle zuerst eine Bestellung + create_response = client.post("/orders", json=sample_order) + assert create_response.status_code == 201 + + # Hole alle Bestellungen + response = client.get("/orders") + assert response.status_code == 200 + + orders = response.json() + assert isinstance(orders, list) + assert len(orders) >= 1 + + # Prüfe, dass unsere Bestellung dabei ist + order_ids = [order["id"] for order in orders] + assert create_response.json()["id"] in order_ids + + def test_delete_order_success(self, client, sample_order): + """Test erfolgreiche Bestelllöschung""" + # Erstelle zuerst eine Bestellung + create_response = client.post("/orders", json=sample_order) + assert create_response.status_code == 201 + order_id = create_response.json()["id"] + + # Lösche die Bestellung + response = client.delete(f"/orders/{order_id}") + assert response.status_code == 200 + assert f"Bestellung {order_id} wurde erfolgreich gelöscht" in response.json()["message"] + + # Prüfe, dass die Bestellung wirklich gelöscht wurde + get_response = client.get("/orders") + orders = get_response.json() + order_ids = [order["id"] for order in orders] + assert order_id not in order_ids + + def test_delete_order_not_found(self, client): + """Test Löschung einer nicht existierenden Bestellung""" + response = client.delete("/orders/non-existent-id") + assert response.status_code == 404 + assert "Bestellung nicht gefunden" in response.json()["detail"] + + def test_order_with_default_quantity(self, client): + """Test Bestellung ohne quantity (sollte 1 sein)""" + order_without_quantity = { + "drinks": [ + { + "drink_type": "Tschunk", + "mate_type": "Club Mate" + # quantity wird nicht angegeben + } + ] + } + + response = client.post("/orders", json=order_without_quantity) + assert response.status_code == 201 + + order = response.json() + assert order["drinks"][0]["quantity"] == 1 + + def test_order_with_notes(self, client): + """Test Bestellung mit Anmerkungen""" + order_with_notes = { + "drinks": [ + { + "drink_type": "Virgin Tschunk", + "mate_type": "Flora Mate", + "quantity": 1, + "notes": "Bitte extra kalt servieren" + } + ] + } + + response = client.post("/orders", json=order_with_notes) + assert response.status_code == 201 + + order = response.json() + assert order["drinks"][0]["notes"] == "Bitte extra kalt servieren" + + +class TestWebSocket: + """Test-Klasse für WebSocket-Funktionalität""" + + @pytest.mark.asyncio + async def test_websocket_connection(self): + """Test WebSocket-Verbindung und initiale Nachricht""" + from websocket_manager import websocket_manager + + # Erstelle einen Test-WebSocket + class MockWebSocket: + def __init__(self): + self.messages = [] + self.closed = False + + async def accept(self): + pass + + async def send_text(self, message): + self.messages.append(message) + + async def receive_text(self): + return "ping" + + async def close(self): + self.closed = True + + # Teste WebSocket-Manager + mock_ws = MockWebSocket() + + # Verbinde + await websocket_manager.connect(mock_ws) + assert len(websocket_manager.active_connections) == 1 + + # Teste Broadcast + test_message = {"type": "test", "data": "test"} + await websocket_manager.broadcast(test_message) + assert len(mock_ws.messages) == 1 + + # Teste spezifische Broadcasts + await websocket_manager.broadcast_order_created({"id": "test", "drinks": []}) + assert len(mock_ws.messages) == 2 + + await websocket_manager.broadcast_order_deleted("test-id") + assert len(mock_ws.messages) == 3 + + # Trenne Verbindung + websocket_manager.disconnect(mock_ws) + assert len(websocket_manager.active_connections) == 0 + + @pytest.mark.asyncio + async def test_websocket_integration_create_order(self): + """Test WebSocket-Broadcast bei Bestellerstellung über API""" + from websocket_manager import websocket_manager + from database import OrderDatabase + from models import Drink, DrinkType, MateType + + # Erstelle einen Test-WebSocket + class MockWebSocket: + def __init__(self): + self.messages = [] + self.closed = False + + async def accept(self): + pass + + async def send_text(self, message): + self.messages.append(message) + + async def receive_text(self): + return "ping" + + async def close(self): + self.closed = True + + # Verbinde Mock-WebSocket + mock_ws = MockWebSocket() + await websocket_manager.connect(mock_ws) + + # Erstelle Test-Datenbank + test_db = OrderDatabase() + + # Erstelle Bestellung über Datenbank (sollte WebSocket-Broadcast auslösen) + drinks = [ + Drink( + drink_type=DrinkType.TSCHUNK, + mate_type=MateType.CLUB_MATE, + quantity=2, + notes="Test-Bestellung" + ) + ] + + order = await test_db.create_order(drinks) + + # Prüfe, dass WebSocket-Nachricht gesendet wurde + assert len(mock_ws.messages) >= 1 + + # Finde die "order_created" Nachricht + order_created_message = None + for message in mock_ws.messages: + try: + data = json.loads(message) + if data.get("type") == "order_created": + order_created_message = data + break + except json.JSONDecodeError: + continue + + assert order_created_message is not None + assert order_created_message["type"] == "order_created" + assert "order" in order_created_message + assert order_created_message["order"]["id"] == order.id + + # Cleanup + websocket_manager.disconnect(mock_ws) + + @pytest.mark.asyncio + async def test_websocket_integration_delete_order(self): + """Test WebSocket-Broadcast bei Bestelllöschung über API""" + from websocket_manager import websocket_manager + from database import OrderDatabase + from models import Drink, DrinkType, MateType + + # Erstelle einen Test-WebSocket + class MockWebSocket: + def __init__(self): + self.messages = [] + self.closed = False + + async def accept(self): + pass + + async def send_text(self, message): + self.messages.append(message) + + async def receive_text(self): + return "ping" + + async def close(self): + self.closed = True + + # Verbinde Mock-WebSocket + mock_ws = MockWebSocket() + await websocket_manager.connect(mock_ws) + + # Erstelle Test-Datenbank + test_db = OrderDatabase() + + # Erstelle zuerst eine Bestellung + drinks = [ + Drink( + drink_type=DrinkType.VIRGIN_TSCHUNK, + mate_type=MateType.FLORA_MATE, + quantity=1 + ) + ] + + order = await test_db.create_order(drinks) + order_id = order.id + + # Lösche die Bestellung (sollte WebSocket-Broadcast auslösen) + success = await test_db.delete_order(order_id) + assert success is True + + # Prüfe, dass WebSocket-Nachricht gesendet wurde + assert len(mock_ws.messages) >= 2 # Mindestens create + delete + + # Finde die "order_deleted" Nachricht + order_deleted_message = None + for message in mock_ws.messages: + try: + data = json.loads(message) + if data.get("type") == "order_deleted": + order_deleted_message = data + break + except json.JSONDecodeError: + continue + + assert order_deleted_message is not None + assert order_deleted_message["type"] == "order_deleted" + assert "order_id" in order_deleted_message + assert order_deleted_message["order_id"] == order_id + + # Cleanup + websocket_manager.disconnect(mock_ws) + + @pytest.mark.asyncio + async def test_websocket_multiple_clients(self): + """Test WebSocket-Broadcast an mehrere verbundene Clients""" + from websocket_manager import websocket_manager + from database import OrderDatabase + from models import Drink, DrinkType, MateType + + # Erstelle mehrere Test-WebSockets + class MockWebSocket: + def __init__(self, client_id): + self.client_id = client_id + self.messages = [] + self.closed = False + + async def accept(self): + pass + + async def send_text(self, message): + self.messages.append(message) + + async def receive_text(self): + return "ping" + + async def close(self): + self.closed = True + + # Verbinde mehrere Mock-WebSockets + mock_ws1 = MockWebSocket("client1") + mock_ws2 = MockWebSocket("client2") + mock_ws3 = MockWebSocket("client3") + + await websocket_manager.connect(mock_ws1) + await websocket_manager.connect(mock_ws2) + await websocket_manager.connect(mock_ws3) + + assert len(websocket_manager.active_connections) == 3 + + # Erstelle Test-Datenbank + test_db = OrderDatabase() + + # Erstelle Bestellung (sollte an alle Clients broadcasten) + drinks = [ + Drink( + drink_type=DrinkType.TSCHUNK_HANNOVER_MISCHE, + mate_type=MateType.CLUB_MATE, + quantity=3, + notes="Multi-Client Test" + ) + ] + + order = await test_db.create_order(drinks) + + # Prüfe, dass alle Clients die Nachricht erhalten haben + for mock_ws in [mock_ws1, mock_ws2, mock_ws3]: + assert len(mock_ws.messages) >= 1 + + # Finde die "order_created" Nachricht + order_created_found = False + for message in mock_ws.messages: + try: + data = json.loads(message) + if data.get("type") == "order_created": + order_created_found = True + assert data["order"]["id"] == order.id + break + except json.JSONDecodeError: + continue + + assert order_created_found, f"Client {mock_ws.client_id} hat keine order_created Nachricht erhalten" + + # Cleanup + websocket_manager.disconnect(mock_ws1) + websocket_manager.disconnect(mock_ws2) + websocket_manager.disconnect(mock_ws3) + + +class TestDatabase: + """Test-Klasse für Datenbankfunktionalität""" + + @pytest.mark.asyncio + async def test_database_operations(self): + """Test Datenbankoperationen""" + from database import OrderDatabase + from models import Drink, DrinkType, MateType + + # Erstelle neue Datenbank-Instanz für Tests + test_db = OrderDatabase() + + # Teste Bestellerstellung + drinks = [ + Drink( + drink_type=DrinkType.TSCHUNK, + mate_type=MateType.CLUB_MATE, + quantity=2, + notes="Test" + ) + ] + + order = await test_db.create_order(drinks) + assert order.id is not None + assert len(order.drinks) == 1 + assert order.drinks[0].drink_type == DrinkType.TSCHUNK + + # Teste Abrufen aller Bestellungen + all_orders = test_db.get_all_orders() + assert len(all_orders) == 1 + assert all_orders[0].id == order.id + + # Teste Abrufen spezifischer Bestellung + retrieved_order = test_db.get_order_by_id(order.id) + assert retrieved_order is not None + assert retrieved_order.id == order.id + + # Teste Löschung + success = await test_db.delete_order(order.id) + assert success is True + + # Prüfe, dass Bestellung gelöscht wurde + all_orders_after = test_db.get_all_orders() + assert len(all_orders_after) == 0 + + # Teste Löschung nicht existierender Bestellung + success = await test_db.delete_order("non-existent") + assert success is False + + +def run_all_tests(): + """Führe alle Tests aus""" + import sys + import subprocess + + print("🧪 Starte automatisierte Tests...") + print("=" * 50) + + # Führe pytest aus + result = subprocess.run([ + sys.executable, "-m", "pytest", + "test_automated.py", + "-v", + "--tb=short" + ], capture_output=True, text=True) + + print(result.stdout) + if result.stderr: + print("STDERR:", result.stderr) + + print("=" * 50) + if result.returncode == 0: + print("✅ Alle Tests erfolgreich!") + else: + print("❌ Einige Tests fehlgeschlagen!") + + return result.returncode == 0 + + +if __name__ == "__main__": + success = run_all_tests() + exit(0 if success else 1) \ No newline at end of file diff --git a/backend/websocket_manager.py b/backend/websocket_manager.py new file mode 100644 index 0000000..6e1f6c6 --- /dev/null +++ b/backend/websocket_manager.py @@ -0,0 +1,74 @@ +from fastapi import WebSocket +from typing import List, Dict, Any +import json +import asyncio +from datetime import datetime + + +class WebSocketManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + """Neue WebSocket-Verbindung hinzufügen.""" + await websocket.accept() + self.active_connections.append(websocket) + print(f"WebSocket verbunden. Aktive Verbindungen: {len(self.active_connections)}") + + def disconnect(self, websocket: WebSocket): + """WebSocket-Verbindung entfernen.""" + if websocket in self.active_connections: + self.active_connections.remove(websocket) + print(f"WebSocket getrennt. Aktive Verbindungen: {len(self.active_connections)}") + + async def broadcast(self, message: Dict[str, Any]): + """Nachricht an alle verbundenen Clients senden.""" + if not self.active_connections: + return + + # Nachricht als JSON serialisieren + message_json = json.dumps(message, default=str) + + # An alle verbundenen Clients senden + disconnected = [] + for connection in self.active_connections: + try: + await connection.send_text(message_json) + except Exception as e: + print(f"Fehler beim Senden an WebSocket: {e}") + disconnected.append(connection) + + # Getrennte Verbindungen entfernen + for connection in disconnected: + self.disconnect(connection) + + async def broadcast_order_created(self, order: Dict[str, Any]): + """Broadcast für neue Bestellung.""" + message = { + "type": "order_created", + "timestamp": datetime.now().isoformat(), + "order": order + } + await self.broadcast(message) + + async def broadcast_order_deleted(self, order_id: str): + """Broadcast für gelöschte Bestellung.""" + message = { + "type": "order_deleted", + "timestamp": datetime.now().isoformat(), + "order_id": order_id + } + await self.broadcast(message) + + async def broadcast_all_orders(self, orders: List[Dict[str, Any]]): + """Broadcast für alle Bestellungen (z.B. bei initialer Verbindung).""" + message = { + "type": "all_orders", + "timestamp": datetime.now().isoformat(), + "orders": orders + } + await self.broadcast(message) + + +# Globale WebSocket-Manager-Instanz +websocket_manager = WebSocketManager() \ No newline at end of file