""" 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"])