This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_edu_search_seeds.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

590 lines
19 KiB
Python

"""
Tests for EduSearch Seeds API.
Tests cover:
- CRUD operations for seeds
- Category management
- Bulk import functionality
- Statistics endpoint
- Error handling
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import HTTPException
from fastapi.testclient import TestClient
import uuid
from datetime import datetime
class AsyncContextManagerMock:
"""Helper class to create proper async context managers for testing."""
def __init__(self, return_value):
self.return_value = return_value
async def __aenter__(self):
return self.return_value
async def __aexit__(self, exc_type, exc_val, exc_tb):
return None
def create_mock_pool_and_conn():
"""Helper to create properly configured mock pool and connection for async tests."""
mock_pool = MagicMock()
mock_conn = AsyncMock()
# Configure pool.acquire() to return an async context manager
mock_pool.acquire.return_value = AsyncContextManagerMock(mock_conn)
return mock_pool, mock_conn
def mock_db_pool_patch(mock_pool):
"""Create a patch for get_db_pool that returns mock_pool."""
async def _mock_get_pool():
return mock_pool
return patch("llm_gateway.routes.edu_search_seeds.get_db_pool", new=_mock_get_pool)
class TestSeedModels:
"""Test Pydantic models for seeds."""
def test_seed_base_valid(self):
"""Test SeedBase with valid data."""
from llm_gateway.routes.edu_search_seeds import SeedBase
seed = SeedBase(
url="https://www.kmk.org",
name="KMK",
description="Test description",
trust_boost=0.95,
enabled=True
)
assert seed.url == "https://www.kmk.org"
assert seed.name == "KMK"
assert seed.trust_boost == 0.95
def test_seed_base_defaults(self):
"""Test SeedBase default values."""
from llm_gateway.routes.edu_search_seeds import SeedBase
seed = SeedBase(
url="https://test.de",
name="Test"
)
assert seed.description is None
assert seed.trust_boost == 0.5
assert seed.enabled is True
assert seed.source_type == "GOV"
assert seed.scope == "FEDERAL"
def test_seed_create_model(self):
"""Test SeedCreate model."""
from llm_gateway.routes.edu_search_seeds import SeedCreate
seed = SeedCreate(
url="https://www.test.de",
name="Test Seed",
category_name="federal"
)
assert seed.url == "https://www.test.de"
assert seed.category_name == "federal"
def test_seed_update_model(self):
"""Test SeedUpdate with partial data."""
from llm_gateway.routes.edu_search_seeds import SeedUpdate
seed = SeedUpdate(enabled=False)
assert seed.enabled is False
assert seed.name is None
assert seed.url is None
class TestCategoryResponse:
"""Test category response models."""
def test_category_response(self):
"""Test CategoryResponse model."""
from llm_gateway.routes.edu_search_seeds import CategoryResponse
cat = CategoryResponse(
id="550e8400-e29b-41d4-a716-446655440000",
name="federal",
display_name="Bundesebene",
description="KMK, BMBF",
icon="icon",
sort_order=0,
is_active=True
)
assert cat.name == "federal"
assert cat.display_name == "Bundesebene"
class TestDatabaseConnection:
"""Test database connection handling."""
@pytest.mark.asyncio
async def test_get_db_pool_creates_pool(self):
"""Test that get_db_pool creates a connection pool."""
from llm_gateway.routes.edu_search_seeds import get_db_pool
import llm_gateway.routes.edu_search_seeds as module
mock_pool = MagicMock()
# Reset global pool
module._pool = None
with patch("llm_gateway.routes.edu_search_seeds.asyncpg.create_pool",
new=AsyncMock(return_value=mock_pool)) as mock_create:
with patch.dict("os.environ", {"DATABASE_URL": "postgresql://test:test@localhost/test"}):
pool = await get_db_pool()
mock_create.assert_called_once()
assert pool == mock_pool
# Cleanup
module._pool = None
@pytest.mark.asyncio
async def test_get_db_pool_reuses_existing(self):
"""Test that get_db_pool reuses existing pool."""
from llm_gateway.routes.edu_search_seeds import get_db_pool
import llm_gateway.routes.edu_search_seeds as module
mock_pool = MagicMock()
module._pool = mock_pool
pool = await get_db_pool()
assert pool == mock_pool
# Cleanup
module._pool = None
class TestListCategories:
"""Test list_categories endpoint."""
@pytest.mark.asyncio
async def test_list_categories_success(self):
"""Test successful category listing."""
from llm_gateway.routes.edu_search_seeds import list_categories
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetch.return_value = [
{
"id": uuid.uuid4(),
"name": "federal",
"display_name": "Bundesebene",
"description": "Test",
"icon": "icon",
"sort_order": 0,
"is_active": True,
"created_at": "2024-01-01T00:00:00Z"
}
]
with mock_db_pool_patch(mock_pool):
result = await list_categories()
assert len(result) == 1
assert result[0].name == "federal"
@pytest.mark.asyncio
async def test_list_categories_empty(self):
"""Test category listing with no categories."""
from llm_gateway.routes.edu_search_seeds import list_categories
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetch.return_value = []
with mock_db_pool_patch(mock_pool):
result = await list_categories()
assert result == []
class TestGetSeed:
"""Test get_seed endpoint."""
@pytest.mark.asyncio
async def test_get_seed_found(self):
"""Test getting existing seed."""
from llm_gateway.routes.edu_search_seeds import get_seed
seed_id = str(uuid.uuid4())
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetchrow.return_value = {
"id": seed_id,
"url": "https://test.de",
"name": "Test",
"description": None,
"category": "federal",
"category_display_name": "Bundesebene",
"source_type": "GOV",
"scope": "FEDERAL",
"state": None,
"trust_boost": 0.5,
"enabled": True,
"crawl_depth": 2,
"crawl_frequency": "weekly",
"last_crawled_at": None,
"last_crawl_status": None,
"last_crawl_docs": 0,
"total_documents": 0,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
with mock_db_pool_patch(mock_pool):
result = await get_seed(seed_id)
assert result.url == "https://test.de"
assert result.category == "federal"
@pytest.mark.asyncio
async def test_get_seed_not_found(self):
"""Test getting non-existing seed returns 404."""
from llm_gateway.routes.edu_search_seeds import get_seed
seed_id = str(uuid.uuid4())
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetchrow.return_value = None
with mock_db_pool_patch(mock_pool):
with pytest.raises(HTTPException) as exc_info:
await get_seed(seed_id)
assert exc_info.value.status_code == 404
class TestCreateSeed:
"""Test create_seed endpoint."""
@pytest.mark.asyncio
async def test_create_seed_success(self):
"""Test successful seed creation."""
from llm_gateway.routes.edu_search_seeds import create_seed, SeedCreate
new_seed = SeedCreate(
url="https://new-seed.de",
name="New Seed",
description="Test seed",
trust_boost=0.8
)
mock_pool, mock_conn = create_mock_pool_and_conn()
new_id = uuid.uuid4()
now = datetime.now()
# Mock for fetchval (category lookup - returns None since no category)
mock_conn.fetchval.return_value = None
# Mock for fetchrow (insert returning)
mock_conn.fetchrow.return_value = {
"id": new_id,
"created_at": now,
"updated_at": now
}
with mock_db_pool_patch(mock_pool):
result = await create_seed(new_seed)
assert result.id == str(new_id)
assert result.url == "https://new-seed.de"
@pytest.mark.asyncio
async def test_create_seed_duplicate_url(self):
"""Test creating seed with duplicate URL fails with 409."""
from llm_gateway.routes.edu_search_seeds import create_seed, SeedCreate
import asyncpg
new_seed = SeedCreate(
url="https://duplicate.de",
name="Duplicate"
)
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetchval.return_value = None # No category
mock_conn.fetchrow.side_effect = asyncpg.UniqueViolationError("duplicate key")
with mock_db_pool_patch(mock_pool):
with pytest.raises(HTTPException) as exc_info:
await create_seed(new_seed)
assert exc_info.value.status_code == 409
assert "existiert bereits" in exc_info.value.detail
class TestUpdateSeed:
"""Test update_seed endpoint."""
@pytest.mark.asyncio
async def test_update_seed_success(self):
"""Test successful seed update."""
from llm_gateway.routes.edu_search_seeds import update_seed, SeedUpdate
seed_id = str(uuid.uuid4())
update_data = SeedUpdate(name="Updated Name")
mock_pool, mock_conn = create_mock_pool_and_conn()
now = datetime.now()
# Mock for execute (update) - returns non-zero
mock_conn.execute.return_value = "UPDATE 1"
# Mock for fetchval (check if update succeeded)
mock_conn.fetchval.return_value = seed_id
# Mock for fetchrow (get_seed after update)
mock_conn.fetchrow.return_value = {
"id": seed_id,
"url": "https://test.de",
"name": "Updated Name",
"description": None,
"category": "federal",
"category_display_name": "Bundesebene",
"source_type": "GOV",
"scope": "FEDERAL",
"state": None,
"trust_boost": 0.5,
"enabled": True,
"crawl_depth": 2,
"crawl_frequency": "weekly",
"last_crawled_at": None,
"last_crawl_status": None,
"last_crawl_docs": 0,
"total_documents": 0,
"created_at": now,
"updated_at": now
}
with mock_db_pool_patch(mock_pool):
result = await update_seed(seed_id, update_data)
assert result.name == "Updated Name"
@pytest.mark.asyncio
async def test_update_seed_not_found(self):
"""Test updating non-existing seed returns 404."""
from llm_gateway.routes.edu_search_seeds import update_seed, SeedUpdate
seed_id = str(uuid.uuid4())
update_data = SeedUpdate(name="Updated")
mock_pool, mock_conn = create_mock_pool_and_conn()
# UPDATE uses fetchrow with RETURNING id - returns None for not found
mock_conn.fetchrow.return_value = None
with mock_db_pool_patch(mock_pool):
with pytest.raises(HTTPException) as exc_info:
await update_seed(seed_id, update_data)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_update_seed_empty_update(self):
"""Test update with no fields returns 400."""
from llm_gateway.routes.edu_search_seeds import update_seed, SeedUpdate
seed_id = str(uuid.uuid4())
update_data = SeedUpdate()
mock_pool, mock_conn = create_mock_pool_and_conn()
with mock_db_pool_patch(mock_pool):
with pytest.raises(HTTPException) as exc_info:
await update_seed(seed_id, update_data)
assert exc_info.value.status_code == 400
class TestDeleteSeed:
"""Test delete_seed endpoint."""
@pytest.mark.asyncio
async def test_delete_seed_success(self):
"""Test successful seed deletion."""
from llm_gateway.routes.edu_search_seeds import delete_seed
seed_id = str(uuid.uuid4())
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.execute.return_value = "DELETE 1"
with mock_db_pool_patch(mock_pool):
result = await delete_seed(seed_id)
assert result["status"] == "deleted"
assert result["id"] == seed_id
@pytest.mark.asyncio
async def test_delete_seed_not_found(self):
"""Test deleting non-existing seed returns 404."""
from llm_gateway.routes.edu_search_seeds import delete_seed
seed_id = str(uuid.uuid4())
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.execute.return_value = "DELETE 0"
with mock_db_pool_patch(mock_pool):
with pytest.raises(HTTPException) as exc_info:
await delete_seed(seed_id)
assert exc_info.value.status_code == 404
class TestBulkImport:
"""Test bulk_import_seeds endpoint."""
@pytest.mark.asyncio
async def test_bulk_import_success(self):
"""Test successful bulk import."""
from llm_gateway.routes.edu_search_seeds import bulk_import_seeds, BulkImportRequest, SeedCreate
seeds = [
SeedCreate(url="https://test1.de", name="Test 1", category_name="federal"),
SeedCreate(url="https://test2.de", name="Test 2", category_name="states")
]
request = BulkImportRequest(seeds=seeds)
mock_pool, mock_conn = create_mock_pool_and_conn()
# Mock category pre-fetch
mock_conn.fetch.return_value = [
{"id": uuid.uuid4(), "name": "federal"},
{"id": uuid.uuid4(), "name": "states"}
]
# Mock inserts - ON CONFLICT DO NOTHING, no exception
mock_conn.execute.return_value = "INSERT 0 1"
with mock_db_pool_patch(mock_pool):
result = await bulk_import_seeds(request)
assert result.imported == 2
assert result.skipped == 0
@pytest.mark.asyncio
async def test_bulk_import_with_errors(self):
"""Test bulk import handles errors gracefully."""
from llm_gateway.routes.edu_search_seeds import bulk_import_seeds, BulkImportRequest, SeedCreate
seeds = [SeedCreate(url="https://error.de", name="Error Seed")]
request = BulkImportRequest(seeds=seeds)
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetch.return_value = [] # No categories
mock_conn.execute.side_effect = Exception("Database error")
with mock_db_pool_patch(mock_pool):
result = await bulk_import_seeds(request)
assert result.imported == 0
assert len(result.errors) == 1
class TestGetStats:
"""Test get_stats endpoint."""
@pytest.mark.asyncio
async def test_get_stats_success(self):
"""Test successful stats retrieval."""
from llm_gateway.routes.edu_search_seeds import get_stats
mock_pool, mock_conn = create_mock_pool_and_conn()
# Mock for multiple fetchval calls
mock_conn.fetchval.side_effect = [56, 52, 1000, None] # total, enabled, total_docs, last_crawl
# Mock for fetch calls (by_category, by_state)
mock_conn.fetch.side_effect = [
[{"name": "federal", "count": 5}], # per category
[{"state": "BY", "count": 5}] # per state
]
with mock_db_pool_patch(mock_pool):
result = await get_stats()
assert result.total_seeds == 56
assert result.enabled_seeds == 52
assert result.total_documents == 1000
class TestExportForCrawler:
"""Test export_seeds_for_crawler endpoint."""
@pytest.mark.asyncio
async def test_export_for_crawler_success(self):
"""Test successful crawler export."""
from llm_gateway.routes.edu_search_seeds import export_seeds_for_crawler
mock_pool, mock_conn = create_mock_pool_and_conn()
mock_conn.fetch.return_value = [
{
"url": "https://test.de",
"trust_boost": 0.9,
"source_type": "GOV",
"scope": "FEDERAL",
"state": None,
"crawl_depth": 2,
"category": "federal"
}
]
with mock_db_pool_patch(mock_pool):
result = await export_seeds_for_crawler()
assert "seeds" in result
assert len(result["seeds"]) == 1
assert "exported_at" in result
assert result["total"] == 1
assert result["seeds"][0]["trust"] == 0.9
class TestEdgeCases:
"""Test edge cases and error handling."""
@pytest.mark.asyncio
async def test_invalid_uuid_format(self):
"""Test handling of invalid UUID format."""
from llm_gateway.routes.edu_search_seeds import get_seed
import asyncpg
mock_pool, mock_conn = create_mock_pool_and_conn()
# asyncpg raises DataError for invalid UUID
mock_conn.fetchrow.side_effect = asyncpg.DataError("invalid UUID")
with mock_db_pool_patch(mock_pool):
with pytest.raises(asyncpg.DataError):
await get_seed("not-a-uuid")
@pytest.mark.asyncio
async def test_database_connection_error(self):
"""Test handling of database connection errors."""
from llm_gateway.routes.edu_search_seeds import list_categories
async def _failing_get_pool():
raise Exception("Connection failed")
with patch("llm_gateway.routes.edu_search_seeds.get_db_pool", new=_failing_get_pool):
with pytest.raises(Exception) as exc_info:
await list_categories()
assert "Connection failed" in str(exc_info.value)
def test_trust_boost_validation(self):
"""Test trust_boost must be between 0 and 1."""
from llm_gateway.routes.edu_search_seeds import SeedBase
from pydantic import ValidationError
# Valid values
seed = SeedBase(url="https://test.de", name="Test", trust_boost=0.5)
assert seed.trust_boost == 0.5
# Edge values
seed_min = SeedBase(url="https://test.de", name="Test", trust_boost=0.0)
assert seed_min.trust_boost == 0.0
seed_max = SeedBase(url="https://test.de", name="Test", trust_boost=1.0)
assert seed_max.trust_boost == 1.0
# Invalid values
with pytest.raises(ValidationError):
SeedBase(url="https://test.de", name="Test", trust_boost=1.5)
with pytest.raises(ValidationError):
SeedBase(url="https://test.de", name="Test", trust_boost=-0.1)