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:
3
geo-service/tests/__init__.py
Normal file
3
geo-service/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
GeoEdu Service Tests
|
||||
"""
|
||||
271
geo-service/tests/test_aoi.py
Normal file
271
geo-service/tests/test_aoi.py
Normal 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
|
||||
194
geo-service/tests/test_learning.py
Normal file
194
geo-service/tests/test_learning.py
Normal 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
|
||||
283
geo-service/tests/test_tiles.py
Normal file
283
geo-service/tests/test_tiles.py
Normal 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]
|
||||
Reference in New Issue
Block a user