Files
getDomain/domain-api/tests/test_ops_migration_service.py

291 lines
11 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import patch
from app.services.ops_migration_service import execute_ops_migration, preview_ops_migration
class OpsMigrationServiceTestCase(unittest.TestCase):
@patch("app.services.ops_migration_service._collect_remote_checks")
@patch("app.services.ops_migration_service._inspect_target_database")
@patch("app.services.ops_migration_service._find_managed_node")
def test_preview_returns_blocking_reason_when_remote_tools_missing(
self,
mock_find_managed_node,
mock_inspect_target_database,
mock_collect_remote_checks,
) -> None:
mock_find_managed_node.return_value = {
"node_code": "target-a",
"ssh_host": "10.0.0.8",
"ssh_user": "root",
"ssh_port": 22,
"auth_mode": "key",
}
mock_collect_remote_checks.return_value = {
"tools": {
"python3": True,
"node": False,
"npm": False,
"systemctl": True,
"psql": True,
"pg_dump": True,
"curl": True,
},
"paths": {
"repo_exists": True,
"repo_git": True,
"domain_root_exists": True,
"api_root_exists": True,
"web_root_exists": True,
},
"remote_db_config": {
"DB_HOST": "127.0.0.1",
"DB_PORT": "5432",
"DB_DATABASE": "domain",
"DB_USER": "domainuser",
},
"blocking_reasons": ["目标机缺少 node。", "目标机缺少 npm。"],
"warnings": [],
}
mock_inspect_target_database.return_value = {"available": False}
ok, message, data = preview_ops_migration({"target_node_code": "target-a"})
self.assertFalse(ok)
self.assertEqual("迁移预检查未通过", message)
self.assertIn("目标机缺少 node。", data["blocking_reasons"])
self.assertIn("目标机缺少 npm。", data["blocking_reasons"])
@patch("app.services.ops_migration_service._collect_remote_checks")
@patch("app.services.ops_migration_service._inspect_target_database")
@patch("app.services.ops_migration_service._find_managed_node")
def test_preview_uses_remote_env_as_target_db_default(
self,
mock_find_managed_node,
mock_inspect_target_database,
mock_collect_remote_checks,
) -> None:
mock_find_managed_node.return_value = {
"node_code": "target-a",
"ssh_host": "10.0.0.8",
"ssh_user": "root",
"ssh_port": 22,
"auth_mode": "key",
}
mock_collect_remote_checks.return_value = {
"tools": {
"python3": True,
"node": True,
"npm": True,
"systemctl": True,
"psql": True,
"pg_dump": True,
"curl": True,
},
"paths": {
"repo_exists": True,
"repo_git": True,
"domain_root_exists": True,
"api_root_exists": True,
"web_root_exists": True,
},
"git_commit": "abc123",
"remote_db_config": {
"DB_HOST": "127.0.0.1",
"DB_PORT": "5433",
"DB_DATABASE": "domain_remote",
"DB_USER": "remote_user",
},
"blocking_reasons": [],
"warnings": [],
}
mock_inspect_target_database.return_value = {
"available": True,
"has_business_data": False,
}
ok, message, data = preview_ops_migration({"target_node_code": "target-a", "overwrite_database": True})
self.assertTrue(ok)
self.assertEqual("迁移预检查完成", message)
self.assertEqual("domain_remote", data["target_db_config"]["database"])
self.assertEqual("remote_user", data["target_db_config"]["user"])
self.assertEqual(5433, data["target_db_config"]["port"])
self.assertTrue(bool(data["execution_guard"]["token"]))
@patch("app.services.ops_migration_service._run_remote_health_check")
@patch("app.services.ops_migration_service._restart_remote_services")
@patch("app.services.ops_migration_service._build_remote_frontend")
@patch("app.services.ops_migration_service._inspect_target_database")
@patch("app.services.ops_migration_service._collect_remote_checks")
@patch("app.services.ops_migration_service._find_managed_node")
def test_execute_rejects_missing_confirmation_text_for_nonempty_target_db(
self,
mock_find_managed_node,
mock_collect_remote_checks,
mock_inspect_target_database,
mock_build_remote_frontend,
mock_restart_remote_services,
mock_run_remote_health_check,
) -> None:
mock_find_managed_node.return_value = {
"node_code": "target-a",
"ssh_host": "10.0.0.8",
"ssh_user": "root",
"ssh_port": 22,
"auth_mode": "key",
}
mock_collect_remote_checks.return_value = {
"tools": {
"python3": True,
"node": True,
"npm": True,
"systemctl": True,
"psql": True,
"pg_dump": True,
"curl": True,
},
"paths": {
"repo_exists": True,
"repo_git": True,
"domain_root_exists": True,
"api_root_exists": True,
"web_root_exists": True,
},
"git_commit": "abc123",
"remote_db_config": {
"DB_HOST": "127.0.0.1",
"DB_PORT": "5432",
"DB_DATABASE": "domain_remote",
"DB_USER": "remote_user",
},
"blocking_reasons": [],
"warnings": [],
}
mock_inspect_target_database.return_value = {
"available": True,
"database": "domain_remote",
"has_business_data": True,
"public_table_count": 10,
"business_table_count": 5,
}
mock_build_remote_frontend.return_value = (True, "ok", {})
mock_restart_remote_services.return_value = (True, "ok", {})
mock_run_remote_health_check.return_value = (True, "ok", {})
preview_ok, _preview_message, preview_data = preview_ops_migration(
{"target_node_code": "target-a", "overwrite_database": True}
)
self.assertTrue(preview_ok)
token = preview_data["execution_guard"]["token"]
required_confirmation_text = preview_data["execution_guard"]["required_confirmation_text"]
self.assertEqual("OVERWRITE domain_remote", required_confirmation_text)
execute_ok, execute_message, execute_data = execute_ops_migration(
{
"target_node_code": "target-a",
"overwrite_database": True,
"execute_confirmation_token": token,
}
)
self.assertFalse(execute_ok)
self.assertEqual("缺少数据库覆盖确认文案,执行被拒绝。", execute_message)
self.assertIn("missing execute_confirmation_text", execute_data["blocking_reasons"])
@patch("app.services.ops_migration_service._start_migration_dispatch_thread")
@patch("app.services.ops_migration_service.create_ops_job")
@patch("app.services.ops_migration_service._collect_remote_checks")
@patch("app.services.ops_migration_service._inspect_target_database")
@patch("app.services.ops_migration_service._find_managed_node")
def test_execute_creates_async_job_for_long_running_migration(
self,
mock_find_managed_node,
mock_inspect_target_database,
mock_collect_remote_checks,
mock_create_ops_job,
mock_start_thread,
) -> None:
mock_find_managed_node.return_value = {
"node_code": "target-a",
"ssh_host": "10.0.0.8",
"ssh_user": "root",
"ssh_port": 22,
"auth_mode": "key",
}
mock_collect_remote_checks.return_value = {
"tools": {
"python3": True,
"node": True,
"npm": True,
"systemctl": True,
"psql": True,
"pg_dump": True,
"curl": True,
},
"paths": {
"repo_exists": True,
"repo_git": True,
"domain_root_exists": True,
"api_root_exists": True,
"web_root_exists": True,
},
"git_commit": "abc123",
"remote_db_config": {
"DB_HOST": "127.0.0.1",
"DB_PORT": "5432",
"DB_DATABASE": "domain_remote",
"DB_USER": "remote_user",
},
"blocking_reasons": [],
"warnings": [],
}
mock_inspect_target_database.return_value = {
"available": True,
"database": "domain_remote",
"has_business_data": False,
"public_table_count": 0,
"business_table_count": 0,
}
mock_create_ops_job.return_value = (
True,
"ok",
{
"job": {
"id": 88,
"job_code": "ops-20260422160000-abc123",
"action": "migration.execute",
"status": "queued",
"target_node_code": "target-a",
}
},
)
preview_ok, _preview_message, preview_data = preview_ops_migration({"target_node_code": "target-a"})
self.assertTrue(preview_ok)
token = preview_data["execution_guard"]["token"]
execute_ok, execute_message, execute_data = execute_ops_migration(
{
"target_node_code": "target-a",
"execute_confirmation_token": token,
}
)
self.assertTrue(execute_ok)
self.assertEqual("迁移任务已创建,后台开始执行。", execute_message)
self.assertEqual(88, execute_data["job"]["id"])
self.assertEqual("migration.execute", mock_create_ops_job.call_args.args[0]["action"])
self.assertFalse(bool(mock_create_ops_job.call_args.args[0]["run_now"]))
self.assertEqual("control-plane", mock_create_ops_job.call_args.args[0]["execution_mode"])
self.assertEqual("", mock_create_ops_job.call_args.args[0]["payload"]["target_db_password"])
mock_start_thread.assert_called_once_with(88)
if __name__ == "__main__":
unittest.main()