from __future__ import annotations import importlib.util import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] CHECKER_PATH = REPO_ROOT / "scripts" / "check-deploy-preflight.py" def _load_checker(): spec = importlib.util.spec_from_file_location("check_deploy_preflight", CHECKER_PATH) if spec is None or spec.loader is None: raise RuntimeError(f"Cannot load {CHECKER_PATH}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module checker = _load_checker() def _valid_values() -> dict[str, str]: values = { "ENVIRONMENT": "prod", "POSTGRES_PASSWORD": "postgres-owner-secret", "APP_DB_PASSWORD": "postgres-app-secret", "OPENAI_API_KEY": "openai-secret", "SESSION_SECRET": "session-secret-with-enough-entropy", "OAUTH_GOOGLE_CLIENT_ID": "google-client", "OAUTH_GOOGLE_CLIENT_SECRET": "google-secret", "ENGINE_MODE": "openai", "ENGINE_GATEWAY_SHARED_SECRET": "engine-gateway-secret-" + ("g" * 32), "AUTH_DEV_LOGIN_ENABLED": "false", "AUTO_SEED_PERSONAS": "false", "ALLOW_SEED_PERSONA_FALLBACK": "false", "VIGNETTE_VOICE_POC_SAMPLE_TTS": "false", } for index, key in enumerate(checker.INTERNAL_TOKEN_ENV_KEYS, start=1): values[key] = f"token-{index}-" + chr(96 + index) * 40 return values def _write_env(path: Path, values: dict[str, str]) -> None: path.write_text( "\n".join(f"{key}={value}" for key, value in values.items()) + "\n", encoding="utf-8", ) class DeployPreflightEnvTests(unittest.TestCase): def _check( self, values: dict[str, str], *, allow_placeholders: bool = False, deployment_profile: str = "compose", ): with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / ".env" _write_env(path, values) return checker.check_env_file( path, allow_placeholder_secrets=allow_placeholders, deployment_profile=deployment_profile, ) def test_valid_independent_internal_tokens_pass(self) -> None: result = self._check(_valid_values()) self.assertTrue(result.ok, result.detail) def test_gateway_shared_secret_is_optional(self) -> None: values = _valid_values() values.pop("ENGINE_GATEWAY_SHARED_SECRET") result = self._check(values) self.assertTrue(result.ok, result.detail) def test_short_gateway_shared_secret_fails(self) -> None: values = _valid_values() values["ENGINE_GATEWAY_SHARED_SECRET"] = "too-short" result = self._check(values) self.assertFalse(result.ok) self.assertIn("ENGINE_GATEWAY_SHARED_SECRET must contain", result.detail) def test_placeholder_gateway_shared_secret_fails(self) -> None: values = _valid_values() values["ENGINE_GATEWAY_SHARED_SECRET"] = ( "change-me-engine-gateway-secret-with-32-characters" ) result = self._check(values) self.assertFalse(result.ok) self.assertIn("ENGINE_GATEWAY_SHARED_SECRET is placeholder", result.detail) def test_missing_internal_token_fails(self) -> None: values = _valid_values() values.pop("VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN") result = self._check(values) self.assertFalse(result.ok) self.assertIn("VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN missing", result.detail) def test_short_internal_token_fails(self) -> None: values = _valid_values() values["VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN"] = "too-short" result = self._check(values) self.assertFalse(result.ok) self.assertIn("must contain at least 32 characters", result.detail) def test_long_placeholder_internal_token_fails(self) -> None: values = _valid_values() values["VIGNETTE_RUPTURE_INTERNAL_TOKEN"] = "replace-with-a-random-token-of-32-chars" result = self._check(values) self.assertFalse(result.ok) self.assertIn("is placeholder or empty", result.detail) def test_shared_internal_token_fails(self) -> None: values = _valid_values() shared = values["VIGNETTE_RUPTURE_INTERNAL_TOKEN"] values["VIGNETTE_PRACTICE_INTERNAL_TOKEN"] = shared result = self._check(values) self.assertFalse(result.ok) self.assertIn("must be independently revocable", result.detail) def test_example_env_allows_explicit_placeholders(self) -> None: result = checker.check_env_file( REPO_ROOT / "infra" / ".env.example", allow_placeholder_secrets=True, ) self.assertTrue(result.ok, result.detail) def test_direct_runtime_uses_database_url_without_packaging_passwords(self) -> None: values = _valid_values() values.pop("POSTGRES_PASSWORD") values.pop("APP_DB_PASSWORD") values["DATABASE_URL"] = "postgresql://runtime:secret@127.0.0.1/vignette" result = self._check(values, deployment_profile="direct-runtime") self.assertTrue(result.ok, result.detail) def test_direct_runtime_requires_database_url(self) -> None: values = _valid_values() values.pop("POSTGRES_PASSWORD") values.pop("APP_DB_PASSWORD") result = self._check(values, deployment_profile="direct-runtime") self.assertFalse(result.ok) self.assertIn("DATABASE_URL missing", result.detail) def test_root_dockerignore_uses_ascii_buildkit_patterns(self) -> None: active_patterns = [ line.strip() for line in (REPO_ROOT / ".dockerignore").read_text( encoding="utf-8" ).splitlines() if line.strip() and not line.lstrip().startswith("#") ] self.assertTrue( all(pattern.isascii() for pattern in active_patterns), "BuildKit metadata와 호환되도록 .dockerignore 패턴은 ASCII여야 한다.", ) def test_compose_requires_frontend_origin_map_without_nested_json_default(self) -> None: compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text( encoding="utf-8" ) self.assertIn( "FRONTEND_ORIGIN_MAP: '${FRONTEND_ORIGIN_MAP:?set FRONTEND_ORIGIN_MAP in .env}'", compose, ) self.assertNotIn("${FRONTEND_ORIGIN_MAP:-{", compose) def test_compose_healthchecks_require_durable_runtime_readiness(self) -> None: compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text( encoding="utf-8" ) self.assertIn("app.ci_regression_dag_node", compose) self.assertIn("PGPASSWORD=$${APP_DB_PASSWORD}", compose) self.assertIn("start_period: 5m", compose) self.assertIn("payload.get('status') == 'ok'", compose) self.assertIn("payload.get('db') is True", compose) self.assertIn("payload.get('engine') is True", compose) def test_compose_exposes_g8_scheduler_and_drift_trigger_as_separate_opt_ins(self) -> None: compose = (REPO_ROOT / "infra" / "docker-compose.yml").read_text( encoding="utf-8" ) self.assertIn( "VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED: " "${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED:-false}", compose, ) self.assertIn( "VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED: " "${VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED:-false}", compose, ) if __name__ == "__main__": unittest.main()