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>
326 lines
13 KiB
Python
326 lines
13 KiB
Python
"""
|
|
Tests for Dev/Staging/Prod environment configuration.
|
|
|
|
Tests the environment files, Docker Compose configurations, and helper scripts
|
|
to ensure proper environment separation and functionality.
|
|
|
|
Usage:
|
|
cd backend && pytest test_environment_config.py -v
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Dict, List
|
|
import pytest
|
|
|
|
|
|
# Project root directory
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
|
|
|
|
class TestEnvironmentFiles:
|
|
"""Test environment configuration files exist and have correct content."""
|
|
|
|
def test_env_dev_exists(self):
|
|
"""Test that .env.dev file exists."""
|
|
env_file = PROJECT_ROOT / ".env.dev"
|
|
assert env_file.exists(), f".env.dev should exist at {env_file}"
|
|
|
|
def test_env_staging_exists(self):
|
|
"""Test that .env.staging file exists."""
|
|
env_file = PROJECT_ROOT / ".env.staging"
|
|
assert env_file.exists(), f".env.staging should exist at {env_file}"
|
|
|
|
def test_env_example_exists(self):
|
|
"""Test that .env.example file exists."""
|
|
env_file = PROJECT_ROOT / ".env.example"
|
|
assert env_file.exists(), f".env.example should exist at {env_file}"
|
|
|
|
def test_env_dev_content(self):
|
|
"""Test that .env.dev has correct environment settings."""
|
|
env_file = PROJECT_ROOT / ".env.dev"
|
|
if not env_file.exists():
|
|
pytest.skip(".env.dev not found")
|
|
|
|
content = env_file.read_text()
|
|
|
|
# Required settings for development
|
|
assert "ENVIRONMENT=development" in content, "ENVIRONMENT should be development"
|
|
assert "COMPOSE_PROJECT_NAME=breakpilot-dev" in content, "Project name should be breakpilot-dev"
|
|
assert "POSTGRES_DB=breakpilot_dev" in content, "Database should be breakpilot_dev"
|
|
assert "DEBUG=true" in content, "DEBUG should be true in development"
|
|
|
|
def test_env_staging_content(self):
|
|
"""Test that .env.staging has correct environment settings."""
|
|
env_file = PROJECT_ROOT / ".env.staging"
|
|
if not env_file.exists():
|
|
pytest.skip(".env.staging not found")
|
|
|
|
content = env_file.read_text()
|
|
|
|
# Required settings for staging
|
|
assert "ENVIRONMENT=staging" in content, "ENVIRONMENT should be staging"
|
|
assert "COMPOSE_PROJECT_NAME=breakpilot-staging" in content, "Project name should be breakpilot-staging"
|
|
assert "POSTGRES_DB=breakpilot_staging" in content, "Database should be breakpilot_staging"
|
|
assert "DEBUG=false" in content, "DEBUG should be false in staging"
|
|
|
|
def test_env_files_have_different_databases(self):
|
|
"""Test that dev and staging use different database names."""
|
|
env_dev = PROJECT_ROOT / ".env.dev"
|
|
env_staging = PROJECT_ROOT / ".env.staging"
|
|
|
|
if not (env_dev.exists() and env_staging.exists()):
|
|
pytest.skip("Environment files not found")
|
|
|
|
dev_content = env_dev.read_text()
|
|
staging_content = env_staging.read_text()
|
|
|
|
# Extract database names
|
|
dev_db = None
|
|
staging_db = None
|
|
for line in dev_content.split("\n"):
|
|
if line.startswith("POSTGRES_DB="):
|
|
dev_db = line.split("=")[1].strip()
|
|
for line in staging_content.split("\n"):
|
|
if line.startswith("POSTGRES_DB="):
|
|
staging_db = line.split("=")[1].strip()
|
|
|
|
assert dev_db is not None, "POSTGRES_DB not found in .env.dev"
|
|
assert staging_db is not None, "POSTGRES_DB not found in .env.staging"
|
|
assert dev_db != staging_db, f"Dev and staging should use different databases, both use {dev_db}"
|
|
|
|
|
|
class TestDockerComposeFiles:
|
|
"""Test Docker Compose configuration files."""
|
|
|
|
def test_docker_compose_yml_exists(self):
|
|
"""Test that main docker-compose.yml exists."""
|
|
compose_file = PROJECT_ROOT / "docker-compose.yml"
|
|
assert compose_file.exists(), "docker-compose.yml should exist"
|
|
|
|
def test_docker_compose_override_exists(self):
|
|
"""Test that docker-compose.override.yml exists for dev."""
|
|
compose_file = PROJECT_ROOT / "docker-compose.override.yml"
|
|
assert compose_file.exists(), "docker-compose.override.yml should exist for development"
|
|
|
|
def test_docker_compose_staging_exists(self):
|
|
"""Test that docker-compose.staging.yml exists."""
|
|
compose_file = PROJECT_ROOT / "docker-compose.staging.yml"
|
|
assert compose_file.exists(), "docker-compose.staging.yml should exist"
|
|
|
|
def test_docker_compose_override_valid_yaml(self):
|
|
"""Test that docker-compose.override.yml has valid syntax."""
|
|
compose_file = PROJECT_ROOT / "docker-compose.override.yml"
|
|
if not compose_file.exists():
|
|
pytest.skip("docker-compose.override.yml not found")
|
|
|
|
result = subprocess.run(
|
|
["docker", "compose", "-f", "docker-compose.yml", "-f", "docker-compose.override.yml", "config"],
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, f"docker-compose.override.yml syntax error: {result.stderr}"
|
|
|
|
def test_docker_compose_staging_valid_yaml(self):
|
|
"""Test that docker-compose.staging.yml has valid syntax."""
|
|
compose_file = PROJECT_ROOT / "docker-compose.staging.yml"
|
|
if not compose_file.exists():
|
|
pytest.skip("docker-compose.staging.yml not found")
|
|
|
|
result = subprocess.run(
|
|
["docker", "compose", "-f", "docker-compose.yml", "-f", "docker-compose.staging.yml", "config"],
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, f"docker-compose.staging.yml syntax error: {result.stderr}"
|
|
|
|
|
|
class TestHelperScripts:
|
|
"""Test helper scripts for environment management."""
|
|
|
|
def test_env_switch_script_exists(self):
|
|
"""Test that env-switch.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "env-switch.sh"
|
|
assert script.exists(), "scripts/env-switch.sh should exist"
|
|
|
|
def test_env_switch_script_executable(self):
|
|
"""Test that env-switch.sh is executable."""
|
|
script = PROJECT_ROOT / "scripts" / "env-switch.sh"
|
|
if not script.exists():
|
|
pytest.skip("env-switch.sh not found")
|
|
assert os.access(script, os.X_OK), "env-switch.sh should be executable"
|
|
|
|
def test_start_script_exists(self):
|
|
"""Test that start.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "start.sh"
|
|
assert script.exists(), "scripts/start.sh should exist"
|
|
|
|
def test_start_script_executable(self):
|
|
"""Test that start.sh is executable."""
|
|
script = PROJECT_ROOT / "scripts" / "start.sh"
|
|
if not script.exists():
|
|
pytest.skip("start.sh not found")
|
|
assert os.access(script, os.X_OK), "start.sh should be executable"
|
|
|
|
def test_stop_script_exists(self):
|
|
"""Test that stop.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "stop.sh"
|
|
assert script.exists(), "scripts/stop.sh should exist"
|
|
|
|
def test_stop_script_executable(self):
|
|
"""Test that stop.sh is executable."""
|
|
script = PROJECT_ROOT / "scripts" / "stop.sh"
|
|
if not script.exists():
|
|
pytest.skip("stop.sh not found")
|
|
assert os.access(script, os.X_OK), "stop.sh should be executable"
|
|
|
|
def test_promote_script_exists(self):
|
|
"""Test that promote.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "promote.sh"
|
|
assert script.exists(), "scripts/promote.sh should exist"
|
|
|
|
def test_promote_script_executable(self):
|
|
"""Test that promote.sh is executable."""
|
|
script = PROJECT_ROOT / "scripts" / "promote.sh"
|
|
if not script.exists():
|
|
pytest.skip("promote.sh not found")
|
|
assert os.access(script, os.X_OK), "promote.sh should be executable"
|
|
|
|
def test_status_script_exists(self):
|
|
"""Test that status.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "status.sh"
|
|
assert script.exists(), "scripts/status.sh should exist"
|
|
|
|
def test_status_script_executable(self):
|
|
"""Test that status.sh is executable."""
|
|
script = PROJECT_ROOT / "scripts" / "status.sh"
|
|
if not script.exists():
|
|
pytest.skip("status.sh not found")
|
|
assert os.access(script, os.X_OK), "status.sh should be executable"
|
|
|
|
|
|
class TestGitIgnore:
|
|
"""Test .gitignore configuration for environment files."""
|
|
|
|
def test_gitignore_exists(self):
|
|
"""Test that .gitignore exists."""
|
|
gitignore = PROJECT_ROOT / ".gitignore"
|
|
assert gitignore.exists(), ".gitignore should exist"
|
|
|
|
def test_gitignore_excludes_env(self):
|
|
"""Test that .gitignore excludes .env file."""
|
|
gitignore = PROJECT_ROOT / ".gitignore"
|
|
if not gitignore.exists():
|
|
pytest.skip(".gitignore not found")
|
|
|
|
content = gitignore.read_text()
|
|
# Check for .env exclusion pattern
|
|
assert ".env" in content, ".gitignore should exclude .env"
|
|
|
|
def test_gitignore_includes_env_dev(self):
|
|
"""Test that .gitignore includes .env.dev (not excluded)."""
|
|
gitignore = PROJECT_ROOT / ".gitignore"
|
|
if not gitignore.exists():
|
|
pytest.skip(".gitignore not found")
|
|
|
|
content = gitignore.read_text()
|
|
# Check for .env.dev inclusion pattern (negation)
|
|
assert "!.env.dev" in content, ".gitignore should include .env.dev"
|
|
|
|
def test_gitignore_includes_env_staging(self):
|
|
"""Test that .gitignore includes .env.staging (not excluded)."""
|
|
gitignore = PROJECT_ROOT / ".gitignore"
|
|
if not gitignore.exists():
|
|
pytest.skip(".gitignore not found")
|
|
|
|
content = gitignore.read_text()
|
|
# Check for .env.staging inclusion pattern (negation)
|
|
assert "!.env.staging" in content, ".gitignore should include .env.staging"
|
|
|
|
|
|
class TestDocumentation:
|
|
"""Test that environment documentation exists."""
|
|
|
|
def test_environments_md_exists(self):
|
|
"""Test that environments.md documentation exists."""
|
|
doc = PROJECT_ROOT / "docs" / "architecture" / "environments.md"
|
|
assert doc.exists(), "docs/architecture/environments.md should exist"
|
|
|
|
def test_environment_setup_guide_exists(self):
|
|
"""Test that environment-setup.md guide exists."""
|
|
doc = PROJECT_ROOT / "docs" / "guides" / "environment-setup.md"
|
|
assert doc.exists(), "docs/guides/environment-setup.md should exist"
|
|
|
|
def test_test_environment_setup_script_exists(self):
|
|
"""Test that test-environment-setup.sh script exists."""
|
|
script = PROJECT_ROOT / "scripts" / "test-environment-setup.sh"
|
|
assert script.exists(), "scripts/test-environment-setup.sh should exist"
|
|
|
|
|
|
class TestEnvironmentIsolation:
|
|
"""Test environment isolation settings."""
|
|
|
|
@pytest.fixture
|
|
def env_configs(self) -> Dict[str, Dict[str, str]]:
|
|
"""Parse environment files into dictionaries."""
|
|
configs = {}
|
|
for env_name in ["dev", "staging"]:
|
|
env_file = PROJECT_ROOT / f".env.{env_name}"
|
|
if not env_file.exists():
|
|
continue
|
|
config = {}
|
|
for line in env_file.read_text().split("\n"):
|
|
line = line.strip()
|
|
if line and not line.startswith("#") and "=" in line:
|
|
key, value = line.split("=", 1)
|
|
config[key.strip()] = value.strip()
|
|
configs[env_name] = config
|
|
return configs
|
|
|
|
def test_unique_compose_project_names(self, env_configs):
|
|
"""Test that each environment has a unique COMPOSE_PROJECT_NAME."""
|
|
if len(env_configs) < 2:
|
|
pytest.skip("Need at least 2 environment files")
|
|
|
|
project_names = [
|
|
config.get("COMPOSE_PROJECT_NAME")
|
|
for config in env_configs.values()
|
|
if config.get("COMPOSE_PROJECT_NAME")
|
|
]
|
|
assert len(project_names) == len(set(project_names)), "COMPOSE_PROJECT_NAME should be unique per environment"
|
|
|
|
def test_unique_database_names(self, env_configs):
|
|
"""Test that each environment uses a unique database name."""
|
|
if len(env_configs) < 2:
|
|
pytest.skip("Need at least 2 environment files")
|
|
|
|
db_names = [
|
|
config.get("POSTGRES_DB")
|
|
for config in env_configs.values()
|
|
if config.get("POSTGRES_DB")
|
|
]
|
|
assert len(db_names) == len(set(db_names)), "POSTGRES_DB should be unique per environment"
|
|
|
|
def test_debug_disabled_in_staging(self, env_configs):
|
|
"""Test that DEBUG is disabled in staging environment."""
|
|
if "staging" not in env_configs:
|
|
pytest.skip(".env.staging not found")
|
|
|
|
debug_value = env_configs["staging"].get("DEBUG", "").lower()
|
|
assert debug_value == "false", "DEBUG should be false in staging"
|
|
|
|
def test_debug_enabled_in_dev(self, env_configs):
|
|
"""Test that DEBUG is enabled in dev environment."""
|
|
if "dev" not in env_configs:
|
|
pytest.skip(".env.dev not found")
|
|
|
|
debug_value = env_configs["dev"].get("DEBUG", "").lower()
|
|
assert debug_value == "true", "DEBUG should be true in development"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|