added tests
This commit is contained in:
parent
4e359cf3ef
commit
cbe9369712
9 changed files with 960 additions and 150 deletions
|
@ -11,6 +11,8 @@ Eine RESTful API für Tschunk-Bestellungen, entwickelt mit FastAPI.
|
||||||
- Wahl zwischen Flora Mate und Club Mate
|
- Wahl zwischen Flora Mate und Club Mate
|
||||||
- Mengenangabe für jedes Getränk (Standard: 1)
|
- Mengenangabe für jedes Getränk (Standard: 1)
|
||||||
- Anmerkungen und Sonderwünsche für jedes Getränk
|
- Anmerkungen und Sonderwünsche für jedes Getränk
|
||||||
|
- **WebSocket-Unterstützung für Echtzeit-Updates**
|
||||||
|
- **Automatisierte Tests mit pytest**
|
||||||
|
|
||||||
## Verfügbare Getränke
|
## Verfügbare Getränke
|
||||||
|
|
||||||
|
@ -115,6 +117,7 @@ Beispiel-Request:
|
||||||
**Hinweise:**
|
**Hinweise:**
|
||||||
- Das `quantity`-Feld ist optional. Falls nicht angegeben, wird automatisch 1 verwendet.
|
- 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).
|
- 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
|
### 2. Alle Bestellungen abrufen
|
||||||
```
|
```
|
||||||
|
@ -126,11 +129,51 @@ GET /orders
|
||||||
DELETE /orders/{order_id}
|
DELETE /orders/{order_id}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Alle verbundenen WebSocket-Clients erhalten automatisch ein Update.
|
||||||
|
|
||||||
### 4. Verfügbare Getränke anzeigen
|
### 4. Verfügbare Getränke anzeigen
|
||||||
```
|
```
|
||||||
GET /drinks
|
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
|
## Beispiel-Verwendung mit curl
|
||||||
|
|
||||||
### Bestellung erstellen:
|
### Bestellung erstellen:
|
||||||
|
@ -164,10 +207,75 @@ curl -X GET "http://localhost:8000/orders"
|
||||||
curl -X DELETE "http://localhost:8000/orders/{order_id}"
|
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
|
## 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
|
```bash
|
||||||
# Stelle sicher, dass das virtuelle Environment aktiviert ist
|
# Stelle sicher, dass das virtuelle Environment aktiviert ist
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
@ -176,18 +284,47 @@ source venv/bin/activate
|
||||||
python test_api.py
|
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
|
## Projektstruktur
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/
|
backend/
|
||||||
├── main.py # FastAPI-Anwendung
|
├── main.py # FastAPI-Anwendung
|
||||||
├── models.py # Pydantic-Modelle
|
├── models.py # Pydantic-Modelle
|
||||||
├── database.py # In-Memory-Datenbank
|
├── database.py # In-Memory-Datenbank
|
||||||
├── test_api.py # Test-Skript
|
├── websocket_manager.py # WebSocket-Management
|
||||||
├── requirements.txt # Python-Abhängigkeiten
|
├── test_automated.py # Automatisierte Tests (pytest)
|
||||||
├── .gitignore # Git-Ignore-Datei
|
├── test_api.py # Manueller API-Test
|
||||||
├── venv/ # Virtuelles Environment (wird ignoriert)
|
├── test_websocket.py # WebSocket-Test
|
||||||
└── README.md # Diese Datei
|
├── 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
|
## Hinweise
|
||||||
|
@ -198,6 +335,8 @@ backend/
|
||||||
- **Validierung**: Die API validiert automatisch alle Eingaben mit Pydantic
|
- **Validierung**: Die API validiert automatisch alle Eingaben mit Pydantic
|
||||||
- **Mengenangabe**: Jedes Getränk kann eine Menge haben (mindestens 1, Standard: 1)
|
- **Mengenangabe**: Jedes Getränk kann eine Menge haben (mindestens 1, Standard: 1)
|
||||||
- **Anmerkungen**: Jedes Getränk kann optionale Anmerkungen haben (max. 500 Zeichen)
|
- **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
|
## Troubleshooting
|
||||||
|
|
||||||
|
@ -218,3 +357,15 @@ Falls Port 8000 bereits belegt ist, kannst du einen anderen Port verwenden:
|
||||||
```bash
|
```bash
|
||||||
uvicorn main:app --reload --port 8001
|
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`
|
|
@ -2,13 +2,14 @@ from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from models import Order, Drink
|
from models import Order, Drink
|
||||||
|
from websocket_manager import websocket_manager
|
||||||
|
|
||||||
|
|
||||||
class OrderDatabase:
|
class OrderDatabase:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.orders: dict[str, Order] = {}
|
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."""
|
"""Create a new order with the given drinks."""
|
||||||
order_id = str(uuid.uuid4())
|
order_id = str(uuid.uuid4())
|
||||||
order = Order(
|
order = Order(
|
||||||
|
@ -17,6 +18,10 @@ class OrderDatabase:
|
||||||
drinks=drinks
|
drinks=drinks
|
||||||
)
|
)
|
||||||
self.orders[order_id] = order
|
self.orders[order_id] = order
|
||||||
|
|
||||||
|
# WebSocket-Broadcast für neue Bestellung
|
||||||
|
await websocket_manager.broadcast_order_created(order.model_dump())
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def get_all_orders(self) -> List[Order]:
|
def get_all_orders(self) -> List[Order]:
|
||||||
|
@ -27,10 +32,14 @@ class OrderDatabase:
|
||||||
"""Get a specific order by ID."""
|
"""Get a specific order by ID."""
|
||||||
return self.orders.get(order_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."""
|
"""Delete an order by ID. Returns True if successful, False if not found."""
|
||||||
if order_id in self.orders:
|
if order_id in self.orders:
|
||||||
del self.orders[order_id]
|
del self.orders[order_id]
|
||||||
|
|
||||||
|
# WebSocket-Broadcast für gelöschte Bestellung
|
||||||
|
await websocket_manager.broadcast_order_deleted(order_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,83 @@
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
from typing import List
|
from typing import List
|
||||||
from models import CreateOrderRequest, Order, DrinkType, MateType
|
from models import CreateOrderRequest, Order, DrinkType, MateType
|
||||||
from database import db
|
from database import db
|
||||||
|
from websocket_manager import websocket_manager
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Tschunk Order API",
|
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"
|
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)
|
@app.post("/orders", response_model=Order, status_code=201)
|
||||||
async def create_order(order_request: CreateOrderRequest):
|
async def create_order(order_request: CreateOrderRequest):
|
||||||
"""
|
"""
|
||||||
Erstellt eine neue Bestellung.
|
Erstellt eine neue Bestellung.
|
||||||
|
|
||||||
- **drinks**: Liste der Getränke mit Typ und Mate-Sorte
|
- **drinks**: Liste der Getränke mit Typ und Mate-Sorte
|
||||||
|
|
||||||
|
Alle verbundenen WebSocket-Clients erhalten automatisch ein Update.
|
||||||
"""
|
"""
|
||||||
if not order_request.drinks:
|
if not order_request.drinks:
|
||||||
raise HTTPException(status_code=400, detail="Mindestens ein Getränk muss bestellt werden")
|
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
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,8 +96,10 @@ async def delete_order(order_id: str):
|
||||||
Löscht eine spezifische Bestellung.
|
Löscht eine spezifische Bestellung.
|
||||||
|
|
||||||
- **order_id**: ID der zu löschenden 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:
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail="Bestellung nicht gefunden")
|
raise HTTPException(status_code=404, detail="Bestellung nicht gefunden")
|
||||||
|
|
||||||
|
|
12
backend/pytest.ini
Normal file
12
backend/pytest.ini
Normal 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"')
|
|
@ -3,3 +3,7 @@ uvicorn[standard]>=0.32.0
|
||||||
pydantic>=2.10.0
|
pydantic>=2.10.0
|
||||||
python-multipart>=0.0.20
|
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
83
backend/run_tests.py
Normal 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)
|
|
@ -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
550
backend/test_automated.py
Normal 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)
|
74
backend/websocket_manager.py
Normal file
74
backend/websocket_manager.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue