fix: Restore all files lost during destructive rebase

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>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,3 @@
"""
GeoEdu Service Tests
"""

View File

@@ -0,0 +1,271 @@
"""
Tests for AOI Packager Service
"""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import json
import sys
sys.path.insert(0, '/app')
from services.aoi_packager import AOIPackagerService
class TestAOIPackagerService:
"""Tests for AOI Packager Service."""
@pytest.fixture
def service(self):
"""Create service instance."""
return AOIPackagerService()
def test_calculate_area_km2_small_polygon(self, service):
"""Test area calculation for small polygon."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.19, 47.70],
[9.20, 47.70],
[9.20, 47.71],
[9.19, 47.71],
[9.19, 47.70],
]
],
}
area = service.calculate_area_km2(polygon)
# Should be approximately 1 km² (rough estimate)
assert 0.5 < area < 2.0
def test_calculate_area_km2_mainau(self, service):
"""Test area calculation for Mainau island polygon."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.1875, 47.7055],
[9.1975, 47.7055],
[9.1975, 47.7115],
[9.1875, 47.7115],
[9.1875, 47.7055],
]
],
}
area = service.calculate_area_km2(polygon)
# Mainau template claims ~0.45 km²
assert 0.3 < area < 1.0
def test_is_within_germany_valid(self, service):
"""Test polygon within Germany."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.19, 47.70],
[9.20, 47.70],
[9.20, 47.71],
[9.19, 47.71],
[9.19, 47.70],
]
],
}
assert service.is_within_germany(polygon) == True
def test_is_within_germany_outside(self, service):
"""Test polygon outside Germany."""
# Paris
polygon = {
"type": "Polygon",
"coordinates": [
[
[2.3, 48.8],
[2.4, 48.8],
[2.4, 48.9],
[2.3, 48.9],
[2.3, 48.8],
]
],
}
assert service.is_within_germany(polygon) == False
def test_validate_polygon_valid(self, service):
"""Test valid polygon validation."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.19, 47.70],
[9.20, 47.70],
[9.20, 47.71],
[9.19, 47.71],
[9.19, 47.70],
]
],
}
is_valid, message = service.validate_polygon(polygon)
assert is_valid == True
assert message == "Valid"
def test_validate_polygon_not_closed(self, service):
"""Test polygon validation with unclosed ring."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.19, 47.70],
[9.20, 47.70],
[9.20, 47.71],
[9.19, 47.71],
# Missing closing point
]
],
}
is_valid, message = service.validate_polygon(polygon)
assert is_valid == False
assert "closed" in message.lower()
def test_validate_polygon_wrong_type(self, service):
"""Test polygon validation with wrong geometry type."""
polygon = {
"type": "Point",
"coordinates": [9.19, 47.70],
}
is_valid, message = service.validate_polygon(polygon)
assert is_valid == False
assert "Polygon" in message
def test_estimate_bundle_size_low(self, service):
"""Test bundle size estimation for low quality."""
size = service.estimate_bundle_size_mb(1.0, "low")
assert size == 10.0
def test_estimate_bundle_size_medium(self, service):
"""Test bundle size estimation for medium quality."""
size = service.estimate_bundle_size_mb(1.0, "medium")
assert size == 25.0
def test_estimate_bundle_size_high(self, service):
"""Test bundle size estimation for high quality."""
size = service.estimate_bundle_size_mb(1.0, "high")
assert size == 50.0
def test_estimate_bundle_size_scales_with_area(self, service):
"""Test bundle size scales with area."""
size_1km = service.estimate_bundle_size_mb(1.0, "medium")
size_2km = service.estimate_bundle_size_mb(2.0, "medium")
assert size_2km == size_1km * 2
class TestGeoUtils:
"""Tests for geo utility functions."""
def test_lat_lon_to_tile_berlin(self):
"""Test tile conversion for Berlin."""
from utils.geo_utils import lat_lon_to_tile
# Berlin: 52.52, 13.405
x, y = lat_lon_to_tile(52.52, 13.405, 10)
# At zoom 10, Berlin should be around tile 550, 335
assert 540 < x < 560
assert 325 < y < 345
def test_tile_to_bounds(self):
"""Test tile to bounds conversion."""
from utils.geo_utils import tile_to_bounds
west, south, east, north = tile_to_bounds(10, 550, 335)
# Should return valid bounds
assert west < east
assert south < north
# Should be somewhere in Germany
assert 5 < west < 20
assert 45 < south < 60
def test_calculate_distance(self):
"""Test distance calculation."""
from utils.geo_utils import calculate_distance
# Berlin to Munich: ~504 km
dist = calculate_distance(52.52, 13.405, 48.1351, 11.582)
assert 450000 < dist < 550000 # meters
def test_get_germany_bounds(self):
"""Test Germany bounds."""
from utils.geo_utils import get_germany_bounds
west, south, east, north = get_germany_bounds()
assert west == 5.87
assert south == 47.27
assert east == 15.04
assert north == 55.06
class TestLicenseChecker:
"""Tests for license checker utility."""
def test_osm_source_allowed(self):
"""Test OSM source is allowed."""
from utils.license_checker import LicenseChecker, DataSource
assert LicenseChecker.is_source_allowed(DataSource.OPENSTREETMAP) == True
def test_copernicus_source_allowed(self):
"""Test Copernicus source is allowed."""
from utils.license_checker import LicenseChecker, DataSource
assert LicenseChecker.is_source_allowed(DataSource.COPERNICUS_DEM) == True
def test_google_source_forbidden(self):
"""Test Google source is forbidden."""
from utils.license_checker import LicenseChecker, DataSource
assert LicenseChecker.is_source_allowed(DataSource.GOOGLE) == False
def test_validate_osm_url(self):
"""Test OSM URL validation."""
from utils.license_checker import LicenseChecker
is_allowed, message = LicenseChecker.validate_url(
"https://tile.openstreetmap.org/10/550/335.png"
)
assert is_allowed == True
assert "ALLOWED" in message
def test_validate_google_url(self):
"""Test Google URL validation."""
from utils.license_checker import LicenseChecker
is_allowed, message = LicenseChecker.validate_url(
"https://maps.googleapis.com/maps/api/staticmap"
)
assert is_allowed == False
assert "FORBIDDEN" in message
def test_get_attribution_for_sources(self):
"""Test getting attribution for sources."""
from utils.license_checker import LicenseChecker, DataSource
sources = [DataSource.OPENSTREETMAP, DataSource.COPERNICUS_DEM]
attributions = LicenseChecker.get_attribution_for_sources(sources)
assert len(attributions) == 2
assert any("OpenStreetMap" in a["name"] for a in attributions)
assert any("Copernicus" in a["name"] for a in attributions)
def test_check_commercial_use_allowed(self):
"""Test commercial use check for allowed sources."""
from utils.license_checker import LicenseChecker, DataSource
sources = [DataSource.OPENSTREETMAP, DataSource.COPERNICUS_DEM]
allowed, issues = LicenseChecker.check_commercial_use(sources)
assert allowed == True
assert len(issues) == 0
def test_check_commercial_use_forbidden(self):
"""Test commercial use check with forbidden source."""
from utils.license_checker import LicenseChecker, DataSource
sources = [DataSource.OPENSTREETMAP, DataSource.GOOGLE]
allowed, issues = LicenseChecker.check_commercial_use(sources)
assert allowed == False
assert len(issues) > 0

View File

@@ -0,0 +1,194 @@
"""
Tests for Learning Generator Service
"""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import json
import sys
sys.path.insert(0, '/app')
from services.learning_generator import LearningGeneratorService
from models.learning_node import LearningTheme, NodeType
class TestLearningGeneratorService:
"""Tests for Learning Generator Service."""
@pytest.fixture
def service(self):
"""Create service instance."""
return LearningGeneratorService()
def test_build_generation_prompt_topographie(self, service):
"""Test prompt building for topography theme."""
aoi_info = {
"bounds": {"west": 9.18, "south": 47.70, "east": 9.20, "north": 47.72},
"center": {"latitude": 47.71, "longitude": 9.19},
"area_km2": 0.5,
}
prompt = service._build_generation_prompt(
aoi_info=aoi_info,
theme=LearningTheme.TOPOGRAPHIE,
difficulty="mittel",
node_count=5,
grade_level="7-8",
language="de",
)
assert "Topographie" in prompt or "topographie" in prompt
assert "47.71" in prompt # center latitude
assert "9.19" in prompt # center longitude
assert "5" in prompt # node count
assert "mittel" in prompt # difficulty
assert "7-8" in prompt # grade level
assert "JSON" in prompt
def test_build_generation_prompt_landnutzung(self, service):
"""Test prompt building for land use theme."""
aoi_info = {
"bounds": {"west": 9.18, "south": 47.70, "east": 9.20, "north": 47.72},
"center": {"latitude": 47.71, "longitude": 9.19},
"area_km2": 0.5,
}
prompt = service._build_generation_prompt(
aoi_info=aoi_info,
theme=LearningTheme.LANDNUTZUNG,
difficulty="leicht",
node_count=3,
grade_level=None,
language="de",
)
assert "Landnutzung" in prompt or "landnutzung" in prompt
def test_parse_llm_response_valid(self, service):
"""Test parsing valid LLM response."""
response = """
Here are the learning nodes:
[
{
"title": "Höhenbestimmung",
"position": {"latitude": 47.71, "longitude": 9.19},
"question": "Schätze die Höhe dieses Punktes.",
"hints": ["Schau auf die Vegetation", "Vergleiche mit dem See"],
"answer": "Ca. 500m",
"explanation": "Die Höhe lässt sich...",
"node_type": "question",
"points": 10
}
]
"""
nodes = service._parse_llm_response(
response, "test-aoi-id", LearningTheme.TOPOGRAPHIE
)
assert len(nodes) == 1
assert nodes[0].title == "Höhenbestimmung"
assert nodes[0].aoi_id == "test-aoi-id"
assert nodes[0].theme == LearningTheme.TOPOGRAPHIE
assert nodes[0].points == 10
def test_parse_llm_response_invalid_json(self, service):
"""Test parsing invalid LLM response."""
response = "This is not valid JSON"
nodes = service._parse_llm_response(
response, "test-aoi-id", LearningTheme.TOPOGRAPHIE
)
assert len(nodes) == 0
def test_generate_mock_nodes(self, service):
"""Test mock node generation."""
nodes = service._generate_mock_nodes(
aoi_id="test-aoi-id",
theme=LearningTheme.TOPOGRAPHIE,
difficulty="mittel",
node_count=3,
)
assert len(nodes) == 3
for node in nodes:
assert node.aoi_id == "test-aoi-id"
assert node.theme == LearningTheme.TOPOGRAPHIE
assert node.approved == False
assert len(node.hints) > 0
def test_generate_mock_nodes_orientierung(self, service):
"""Test mock node generation for orientation theme."""
nodes = service._generate_mock_nodes(
aoi_id="test-aoi-id",
theme=LearningTheme.ORIENTIERUNG,
difficulty="leicht",
node_count=2,
)
assert len(nodes) == 2
for node in nodes:
assert node.theme == LearningTheme.ORIENTIERUNG
@pytest.mark.asyncio
async def test_get_nodes_for_aoi_empty(self, service):
"""Test getting nodes for non-existent AOI."""
nodes = await service.get_nodes_for_aoi("nonexistent-aoi")
assert nodes is None
@pytest.mark.asyncio
async def test_get_statistics_empty(self, service):
"""Test statistics with no data."""
stats = await service.get_statistics()
assert stats["total_nodes"] == 0
assert stats["by_theme"] == {}
class TestLearningNodeModel:
"""Tests for Learning Node model."""
def test_learning_theme_values(self):
"""Test all theme enum values exist."""
themes = [
LearningTheme.TOPOGRAPHIE,
LearningTheme.LANDNUTZUNG,
LearningTheme.ORIENTIERUNG,
LearningTheme.GEOLOGIE,
LearningTheme.HYDROLOGIE,
LearningTheme.VEGETATION,
]
assert len(themes) == 6
def test_node_type_values(self):
"""Test all node type enum values exist."""
types = [
NodeType.QUESTION,
NodeType.OBSERVATION,
NodeType.EXPLORATION,
]
assert len(types) == 3
def test_create_learning_node(self):
"""Test creating a learning node."""
from models.learning_node import LearningNode
node = LearningNode(
id="test-id",
aoi_id="test-aoi",
title="Test Station",
theme=LearningTheme.TOPOGRAPHIE,
position={"latitude": 47.71, "longitude": 9.19},
question="Test question?",
hints=["Hint 1", "Hint 2"],
answer="Test answer",
explanation="Test explanation",
node_type=NodeType.QUESTION,
points=10,
approved=False,
)
assert node.id == "test-id"
assert node.title == "Test Station"
assert node.points == 10
assert len(node.hints) == 2

View File

@@ -0,0 +1,283 @@
"""
Tests for Tile Server API
"""
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
# Import app after mocking to avoid import errors
import sys
sys.path.insert(0, '/app')
@pytest.fixture
def client():
"""Create test client."""
from main import app
return TestClient(app)
class TestTileEndpoints:
"""Tests for tile server endpoints."""
def test_health_check(self, client):
"""Test health check endpoint returns healthy status."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert data["service"] == "geo-service"
assert "data_status" in data
def test_root_endpoint(self, client):
"""Test root endpoint returns service info."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert data["service"] == "GeoEdu Service"
assert "endpoints" in data
assert "attribution" in data
def test_tile_metadata(self, client):
"""Test tile metadata endpoint."""
response = client.get("/api/v1/tiles/metadata")
assert response.status_code == 200
data = response.json()
assert "name" in data
assert "minzoom" in data
assert "maxzoom" in data
assert "bounds" in data
assert data["attribution"] == "© OpenStreetMap contributors (ODbL)"
def test_tile_bounds(self, client):
"""Test tile bounds endpoint returns Germany bounds."""
response = client.get("/api/v1/tiles/bounds")
assert response.status_code == 200
data = response.json()
assert "bounds" in data
# Germany approximate bounds
bounds = data["bounds"]
assert bounds[0] < 6 # west
assert bounds[1] > 47 # south
assert bounds[2] > 14 # east
assert bounds[3] < 56 # north
def test_style_json(self, client):
"""Test MapLibre style endpoint."""
response = client.get("/api/v1/tiles/style.json")
assert response.status_code == 200
data = response.json()
assert data["version"] == 8
assert "sources" in data
assert "layers" in data
assert "osm" in data["sources"]
def test_tile_request_without_data(self, client):
"""Test tile request returns 503 when data not available."""
response = client.get("/api/v1/tiles/0/0/0.pbf")
# Should return 503 (data not available) or 204 (empty tile)
assert response.status_code in [204, 503]
def test_tile_request_invalid_zoom(self, client):
"""Test tile request with invalid zoom level."""
response = client.get("/api/v1/tiles/25/0/0.pbf")
assert response.status_code == 422 # Validation error
class TestTerrainEndpoints:
"""Tests for terrain/DEM endpoints."""
def test_terrain_metadata(self, client):
"""Test terrain metadata endpoint."""
response = client.get("/api/v1/terrain/metadata")
assert response.status_code == 200
data = response.json()
assert data["name"] == "Copernicus DEM GLO-30"
assert data["resolution_m"] == 30
assert "bounds" in data
def test_heightmap_request_without_data(self, client):
"""Test heightmap request returns appropriate status when no data."""
response = client.get("/api/v1/terrain/10/500/500.png")
# Should return 204 (no content) or 503 (data not available)
assert response.status_code in [204, 503]
def test_elevation_at_point_within_germany(self, client):
"""Test elevation endpoint with valid Germany coordinates."""
# Berlin coordinates
response = client.get("/api/v1/terrain/elevation?lat=52.52&lon=13.405")
# Should return 404 (no data) or 200 (with elevation) or 503
assert response.status_code in [200, 404, 503]
def test_elevation_at_point_outside_germany(self, client):
"""Test elevation endpoint with coordinates outside Germany."""
# Paris coordinates (outside Germany)
response = client.get("/api/v1/terrain/elevation?lat=48.8566&lon=2.3522")
# Should return 422 (validation error - outside bounds)
assert response.status_code == 422
class TestAOIEndpoints:
"""Tests for AOI (Area of Interest) endpoints."""
def test_validate_polygon_valid(self, client):
"""Test polygon validation with valid polygon."""
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.19, 47.71],
[9.20, 47.71],
[9.20, 47.70],
[9.19, 47.70],
[9.19, 47.71],
]
],
}
response = client.post("/api/v1/aoi/validate", json=polygon)
assert response.status_code == 200
data = response.json()
assert "valid" in data
assert "area_km2" in data
assert "within_germany" in data
def test_validate_polygon_too_large(self, client):
"""Test polygon validation with too large area."""
# Large polygon (> 4 km²)
polygon = {
"type": "Polygon",
"coordinates": [
[
[9.0, 47.5],
[9.5, 47.5],
[9.5, 48.0],
[9.0, 48.0],
[9.0, 47.5],
]
],
}
response = client.post("/api/v1/aoi/validate", json=polygon)
assert response.status_code == 200
data = response.json()
assert data["within_size_limit"] == False
def test_validate_polygon_outside_germany(self, client):
"""Test polygon validation outside Germany."""
# Paris area
polygon = {
"type": "Polygon",
"coordinates": [
[
[2.3, 48.8],
[2.4, 48.8],
[2.4, 48.9],
[2.3, 48.9],
[2.3, 48.8],
]
],
}
response = client.post("/api/v1/aoi/validate", json=polygon)
assert response.status_code == 200
data = response.json()
assert data["within_germany"] == False
def test_mainau_template(self, client):
"""Test Mainau demo template endpoint."""
response = client.get("/api/v1/aoi/templates/mainau")
assert response.status_code == 200
data = response.json()
assert data["name"] == "Insel Mainau"
assert "polygon" in data
assert "center" in data
assert "suggested_themes" in data
def test_create_aoi(self, client):
"""Test AOI creation with valid polygon."""
request = {
"polygon": {
"type": "Polygon",
"coordinates": [
[
[9.1875, 47.7055],
[9.1975, 47.7055],
[9.1975, 47.7115],
[9.1875, 47.7115],
[9.1875, 47.7055],
]
],
},
"theme": "topographie",
"quality": "medium",
}
response = client.post("/api/v1/aoi", json=request)
assert response.status_code == 200
data = response.json()
assert "aoi_id" in data
assert data["status"] == "queued"
assert "area_km2" in data
def test_create_aoi_too_large(self, client):
"""Test AOI creation fails with too large area."""
request = {
"polygon": {
"type": "Polygon",
"coordinates": [
[
[9.0, 47.5],
[9.5, 47.5],
[9.5, 48.0],
[9.0, 48.0],
[9.0, 47.5],
]
],
},
"theme": "topographie",
"quality": "medium",
}
response = client.post("/api/v1/aoi", json=request)
assert response.status_code == 400
def test_get_nonexistent_aoi(self, client):
"""Test getting non-existent AOI returns 404."""
response = client.get("/api/v1/aoi/nonexistent-id")
assert response.status_code == 404
class TestLearningEndpoints:
"""Tests for Learning Nodes endpoints."""
def test_learning_templates(self, client):
"""Test learning templates endpoint."""
response = client.get("/api/v1/learning/templates")
assert response.status_code == 200
data = response.json()
assert "themes" in data
assert "difficulties" in data
assert len(data["themes"]) == 6 # 6 themes
# Check theme structure
theme = data["themes"][0]
assert "id" in theme
assert "name" in theme
assert "description" in theme
assert "example_questions" in theme
def test_learning_statistics(self, client):
"""Test learning statistics endpoint."""
response = client.get("/api/v1/learning/statistics")
assert response.status_code == 200
data = response.json()
assert "total_nodes_generated" in data
assert "nodes_by_theme" in data
def test_generate_nodes_without_aoi(self, client):
"""Test node generation fails without valid AOI."""
request = {
"aoi_id": "nonexistent-aoi",
"theme": "topographie",
"difficulty": "mittel",
"node_count": 5,
}
response = client.post("/api/v1/learning/generate", json=request)
# Should return 404 (AOI not found) or 503 (Ollama not available)
assert response.status_code in [404, 503]