added tests

This commit is contained in:
Jan Felix Wiebe 2025-07-09 22:18:21 +02:00
parent 4e359cf3ef
commit cbe9369712
9 changed files with 960 additions and 150 deletions

View file

@ -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
```
```
### 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`

View file

@ -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

View file

@ -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")

12
backend/pytest.ini Normal file
View file

@ -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"')

View file

@ -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
requests>=2.32.0
websockets>=12.0
pytest>=7.0.0
pytest-asyncio>=0.23.0
httpx>=0.25.0

83
backend/run_tests.py Normal file
View file

@ -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)

View file

@ -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()

550
backend/test_automated.py Normal file
View file

@ -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)

View file

@ -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()