from __future__ import annotations import re import shutil import subprocess import unittest from pathlib import Path DEV_UP_PATH = Path(__file__).with_name("dev-up.ps1") class DevUpDatabaseSafetyTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.source = DEV_UP_PATH.read_text(encoding="utf-8") def test_normal_dev_up_has_no_destructive_database_command(self) -> None: forbidden = ( r"(?im)^\s*(?:&\s*)?docker(?:\.exe)?\s+(?:container\s+)?rm\b", r"(?im)^\s*(?:&\s*)?docker(?:\.exe)?\s+volume\s+(?:rm|prune)\b", r"(?im)^\s*(?:&\s*)?docker(?:\.exe)?\s+compose\s+down\b.*(?:-v|--volumes)", ) for pattern in forbidden: self.assertIsNone(re.search(pattern, self.source), pattern) def test_stopped_container_is_started_before_any_new_container_path(self) -> None: state_probe = self.source.index("$containerState = Get-DevDbContainerState") start_branch = self.source.index("Start-ExistingDevDbContainer $containerState") new_container_path = self.source.index("$apiEnv = Join-Path $api '.env'") self.assertLess(state_probe, start_branch) self.assertLess(start_branch, new_container_path) self.assertIn("& docker start $DevDbContainerName", self.source) self.assertIn("$state -notin @('created', 'exited')", self.source) self.assertIn("dev-up은 기존 DB를 제거하거나 교체하지 않는다", self.source) def test_new_database_uses_a_stable_named_pgdata_volume(self) -> None: self.assertIn( "$DevDbDataVolume = 'vignette-dev-db-pgdata'", self.source ) self.assertIn("docker volume create $DevDbDataVolume", self.source) self.assertIn( "type=volume,source={0},target=/var/lib/postgresql/data", self.source ) self.assertNotIn( "-v', ('{0}:/var/lib/postgresql/data", self.source ) def test_failure_paths_preserve_container_and_volume_for_recovery(self) -> None: required_fail_closed_messages = ( "데이터 교체 없이 중단함", "컨테이너/볼륨을 교체하지 않고 중단함", "named PGDATA volume은 보존함", "조사/복구를 위해 보존함", ) for message in required_fail_closed_messages: self.assertIn(message, self.source) def test_windows_powershell_51_parser_accepts_script(self) -> None: powershell = shutil.which("powershell.exe") if powershell is None: self.skipTest("Windows PowerShell 5.1 is unavailable") escaped_path = str(DEV_UP_PATH.resolve()).replace("'", "''") command = ( "$tokens=$null; $errors=$null; " "[System.Management.Automation.Language.Parser]::ParseFile(" f"'{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; " "if ($errors.Count -gt 0) { " "$errors | ForEach-Object { Write-Error $_.Message }; exit 1 }; exit 0" ) completed = subprocess.run( [powershell, "-NoProfile", "-Command", command], check=False, capture_output=True, text=True, encoding="utf-8", ) self.assertEqual( completed.returncode, 0, msg=f"stdout={completed.stdout}\nstderr={completed.stderr}", ) if __name__ == "__main__": unittest.main()