A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
295 lines
9.7 KiB
Python
295 lines
9.7 KiB
Python
"""
|
|
Tests for Recording API
|
|
|
|
Tests for Jibri webhook handling, recording management, and transcription endpoints.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
# Import the app (adjust import path as needed)
|
|
# In actual test environment, this would be the main FastAPI app
|
|
# from main import app
|
|
|
|
# For now, we create a minimal test setup
|
|
from fastapi import FastAPI
|
|
from recording_api import router as recording_router
|
|
|
|
app = FastAPI()
|
|
app.include_router(recording_router)
|
|
client = TestClient(app)
|
|
|
|
|
|
class TestJibriWebhook:
|
|
"""Tests for Jibri webhook endpoint."""
|
|
|
|
def test_webhook_recording_completed_valid(self):
|
|
"""Test webhook with valid recording_completed event."""
|
|
payload = {
|
|
"event": "recording_completed",
|
|
"recording_name": "test-room_20260115_120000",
|
|
"storage_path": "recordings/test-room_20260115_120000/video.mp4",
|
|
"audio_path": "recordings/test-room_20260115_120000/audio.wav",
|
|
"file_size_bytes": 52428800,
|
|
"timestamp": "2026-01-15T12:00:00Z"
|
|
}
|
|
|
|
response = client.post("/api/recordings/webhook", json=payload)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["status"] == "uploaded"
|
|
assert "recording_id" in data
|
|
assert data["meeting_id"] == "test-room"
|
|
|
|
def test_webhook_unknown_event_rejected(self):
|
|
"""Test that unknown event types are rejected."""
|
|
payload = {
|
|
"event": "unknown_event",
|
|
"recording_name": "test",
|
|
"storage_path": "test/video.mp4",
|
|
"file_size_bytes": 1000,
|
|
"timestamp": "2026-01-15T12:00:00Z"
|
|
}
|
|
|
|
response = client.post("/api/recordings/webhook", json=payload)
|
|
|
|
assert response.status_code == 400
|
|
assert "Unknown event type" in response.json()["error"]
|
|
|
|
def test_webhook_missing_required_fields(self):
|
|
"""Test that missing required fields cause validation error."""
|
|
payload = {
|
|
"event": "recording_completed"
|
|
# Missing other required fields
|
|
}
|
|
|
|
response = client.post("/api/recordings/webhook", json=payload)
|
|
|
|
assert response.status_code == 422 # Validation error
|
|
|
|
|
|
class TestRecordingManagement:
|
|
"""Tests for recording CRUD operations."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create a test recording before each test."""
|
|
# Clear store and create test recording
|
|
from recording_api import _recordings_store
|
|
_recordings_store.clear()
|
|
|
|
payload = {
|
|
"event": "recording_completed",
|
|
"recording_name": "fixture-room_20260115_100000",
|
|
"storage_path": "recordings/fixture-room/video.mp4",
|
|
"file_size_bytes": 10000000,
|
|
"timestamp": "2026-01-15T10:00:00Z"
|
|
}
|
|
response = client.post("/api/recordings/webhook", json=payload)
|
|
self.recording_id = response.json()["recording_id"]
|
|
|
|
def test_list_recordings_empty(self):
|
|
"""Test listing recordings when empty."""
|
|
from recording_api import _recordings_store
|
|
_recordings_store.clear()
|
|
|
|
response = client.get("/api/recordings")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 0
|
|
assert data["recordings"] == []
|
|
|
|
def test_list_recordings_with_data(self):
|
|
"""Test listing recordings returns created recordings."""
|
|
response = client.get("/api/recordings")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 1
|
|
assert len(data["recordings"]) == 1
|
|
|
|
def test_list_recordings_filter_by_status(self):
|
|
"""Test filtering recordings by status."""
|
|
response = client.get("/api/recordings?status=uploaded")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert all(r["status"] == "uploaded" for r in data["recordings"])
|
|
|
|
def test_list_recordings_pagination(self):
|
|
"""Test pagination of recordings list."""
|
|
response = client.get("/api/recordings?page=1&page_size=10")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["page"] == 1
|
|
assert data["page_size"] == 10
|
|
|
|
def test_get_recording_by_id(self):
|
|
"""Test getting a specific recording by ID."""
|
|
response = client.get(f"/api/recordings/{self.recording_id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == self.recording_id
|
|
assert data["status"] == "uploaded"
|
|
|
|
def test_get_recording_not_found(self):
|
|
"""Test getting non-existent recording returns 404."""
|
|
response = client.get("/api/recordings/nonexistent-id")
|
|
|
|
assert response.status_code == 404
|
|
assert "not found" in response.json()["detail"].lower()
|
|
|
|
def test_delete_recording(self):
|
|
"""Test soft-deleting a recording."""
|
|
response = client.delete(
|
|
f"/api/recordings/{self.recording_id}?reason=DSGVO%20request"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["status"] == "deleted"
|
|
|
|
def test_delete_recording_requires_reason(self):
|
|
"""Test that deletion requires a reason."""
|
|
response = client.delete(f"/api/recordings/{self.recording_id}")
|
|
|
|
assert response.status_code == 422 # Missing required query param
|
|
|
|
|
|
class TestTranscriptionEndpoints:
|
|
"""Tests for transcription management."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Create test recording and clear transcription store."""
|
|
from recording_api import _recordings_store, _transcriptions_store
|
|
_recordings_store.clear()
|
|
_transcriptions_store.clear()
|
|
|
|
payload = {
|
|
"event": "recording_completed",
|
|
"recording_name": "trans-test_20260115_110000",
|
|
"storage_path": "recordings/trans-test/video.mp4",
|
|
"file_size_bytes": 5000000,
|
|
"timestamp": "2026-01-15T11:00:00Z"
|
|
}
|
|
response = client.post("/api/recordings/webhook", json=payload)
|
|
self.recording_id = response.json()["recording_id"]
|
|
|
|
def test_start_transcription(self):
|
|
"""Test starting a transcription job."""
|
|
response = client.post(
|
|
f"/api/recordings/{self.recording_id}/transcribe",
|
|
json={"language": "de", "model": "large-v3"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "pending"
|
|
assert data["language"] == "de"
|
|
assert data["model"] == "large-v3"
|
|
|
|
def test_start_transcription_default_values(self):
|
|
"""Test transcription uses default values when not specified."""
|
|
response = client.post(
|
|
f"/api/recordings/{self.recording_id}/transcribe",
|
|
json={}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["language"] == "de"
|
|
assert data["model"] == "large-v3"
|
|
|
|
def test_start_transcription_recording_not_found(self):
|
|
"""Test starting transcription for non-existent recording."""
|
|
response = client.post(
|
|
"/api/recordings/nonexistent/transcribe",
|
|
json={"language": "de"}
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_start_transcription_duplicate_rejected(self):
|
|
"""Test that duplicate transcription requests are rejected."""
|
|
# First request
|
|
client.post(
|
|
f"/api/recordings/{self.recording_id}/transcribe",
|
|
json={"language": "de"}
|
|
)
|
|
|
|
# Second request should fail
|
|
response = client.post(
|
|
f"/api/recordings/{self.recording_id}/transcribe",
|
|
json={"language": "de"}
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
assert "already exists" in response.json()["detail"]
|
|
|
|
def test_get_transcription_status(self):
|
|
"""Test getting transcription status."""
|
|
# Start transcription first
|
|
client.post(
|
|
f"/api/recordings/{self.recording_id}/transcribe",
|
|
json={"language": "de"}
|
|
)
|
|
|
|
response = client.get(
|
|
f"/api/recordings/{self.recording_id}/transcription"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["recording_id"] == self.recording_id
|
|
assert data["status"] == "pending"
|
|
|
|
def test_get_transcription_not_found(self):
|
|
"""Test getting transcription for recording without transcription."""
|
|
response = client.get(
|
|
f"/api/recordings/{self.recording_id}/transcription"
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestAuditLog:
|
|
"""Tests for audit log endpoints."""
|
|
|
|
def test_get_audit_log(self):
|
|
"""Test retrieving audit log entries."""
|
|
response = client.get("/api/recordings/audit/log")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "entries" in data
|
|
assert "total" in data
|
|
|
|
def test_get_audit_log_filter_by_action(self):
|
|
"""Test filtering audit log by action."""
|
|
response = client.get("/api/recordings/audit/log?action=created")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestHealthCheck:
|
|
"""Tests for health check endpoint."""
|
|
|
|
def test_health_check(self):
|
|
"""Test health check returns healthy status."""
|
|
response = client.get("/api/recordings/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "healthy"
|
|
assert "recordings_count" in data
|
|
assert "minio_endpoint" in data
|