mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 18:54:32 +00:00
423 lines
14 KiB
Python
423 lines
14 KiB
Python
"""Tests for installer core classes: state, milestone, controller, build."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from installer.state import InstallerState
|
|
from installer.milestone import Milestone, ProgressCallback
|
|
from installer.build import get_build_command, BuildMilestone
|
|
from installer.core.controller import ProleController
|
|
|
|
|
|
# ===== InstallerState =====
|
|
|
|
class TestInstallerState:
|
|
def test_defaults(self):
|
|
s = InstallerState()
|
|
assert s.current_id is None
|
|
assert s.data == {}
|
|
assert s.inputs == {}
|
|
assert s.config_data == {}
|
|
assert s.errors == []
|
|
assert s.progress == {}
|
|
assert s.history == []
|
|
assert s.completed == set()
|
|
assert s.controller is None
|
|
|
|
def test_mark_completed(self):
|
|
s = InstallerState()
|
|
s.mark_completed('step1')
|
|
s.mark_completed('step2')
|
|
assert 'step1' in s.completed
|
|
assert 'step2' in s.completed
|
|
|
|
def test_mark_completed_idempotent(self):
|
|
s = InstallerState()
|
|
s.mark_completed('step1')
|
|
s.mark_completed('step1')
|
|
assert len(s.completed) == 1
|
|
|
|
def test_with_controller(self):
|
|
s = InstallerState(controller='fake')
|
|
assert s.controller == 'fake'
|
|
|
|
def test_inputs_and_config(self):
|
|
s = InstallerState(
|
|
inputs={'key': 'val'},
|
|
config_data={'section': {'k': 'v'}},
|
|
)
|
|
assert s.inputs['key'] == 'val'
|
|
assert s.config_data['section']['k'] == 'v'
|
|
|
|
|
|
# ===== Milestone base class =====
|
|
|
|
class _DummyMilestone(Milestone):
|
|
"""Concrete subclass for testing the abstract base."""
|
|
def execute(self, state, progress=None):
|
|
if progress:
|
|
progress("running", 0.5)
|
|
|
|
class TestMilestoneBase:
|
|
def test_init(self):
|
|
m = _DummyMilestone('test_id', 'Test Title')
|
|
assert m.id == 'test_id'
|
|
assert m.title == 'Test Title'
|
|
|
|
def test_validate_default(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
assert m.validate(InstallerState()) is None
|
|
|
|
def test_next_default(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
assert m.next(InstallerState()) is None
|
|
|
|
def test_execute_with_progress(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
msgs = []
|
|
m.execute(InstallerState(), progress=lambda msg, p: msgs.append((msg, p)))
|
|
assert ('running', 0.5) in msgs
|
|
|
|
def test_parse_bool(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
assert m._parse_bool('true') is True
|
|
assert m._parse_bool('false') is False
|
|
assert m._parse_bool(None) is False
|
|
assert m._parse_bool(None, default=True) is True
|
|
|
|
def test_run_cmd_list(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
rc = m._run_cmd(['echo', 'hello'])
|
|
assert rc == 0
|
|
|
|
def test_run_cmd_string(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
rc = m._run_cmd('echo hello')
|
|
assert rc == 0
|
|
|
|
def test_run_cmd_with_stdout(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
lines = []
|
|
rc = m._run_cmd(['echo', 'test_output'], on_stdout=lambda l: lines.append(l.strip()))
|
|
assert rc == 0
|
|
assert 'test_output' in lines
|
|
|
|
def test_run_cmd_failure(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
rc = m._run_cmd(['false'])
|
|
assert rc != 0
|
|
|
|
def test_run_cmd_bad_command(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
rc = m._run_cmd(['/nonexistent/command'])
|
|
assert rc == 1
|
|
|
|
def test_get_script_env_basic(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(
|
|
inputs={
|
|
'init_password.db_namespace': 'myns',
|
|
'init_password.db_username': 'testuser',
|
|
'init_password.db_password': 'secret123',
|
|
'init_cluster.cluster_env': 'dev',
|
|
'kerberos_config.realm': 'EXAMPLE.COM',
|
|
'kerberos_config.kdc': '10.0.0.1',
|
|
'kerberos_config.user': 'admin',
|
|
'kerberos_config.password': 'krbpass',
|
|
'kerberos_config.enabled': 'True',
|
|
'init_cluster.at_rest_encryption_enabled': 'true',
|
|
},
|
|
config_data={},
|
|
)
|
|
env = m._get_script_env(state)
|
|
assert env['NAMESPACE'] == 'myns'
|
|
assert env['DB_PASSWORD'] == 'secret123'
|
|
assert env['PROLE_DB_USER'] == 'testuser'
|
|
assert env['PROLE_MODE'] == 'k3d'
|
|
assert env['KRB5_REALM'] == 'EXAMPLE.COM'
|
|
assert env['KERBEROS_ENABLED'] == 'True'
|
|
assert env['AT_REST_ENCRYPTION_ENABLED'] == 'true'
|
|
assert env['REALM'] == 'EXAMPLE.COM'
|
|
assert env['DOMAIN'] == 'example.com'
|
|
|
|
def test_get_script_env_defaults(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(inputs={}, config_data={})
|
|
env = m._get_script_env(state)
|
|
assert env['NAMESPACE'] == 'default'
|
|
assert env['PROLE_MODE'] == 'k3d'
|
|
assert env['KERBEROS_ENABLED'] == 'False'
|
|
|
|
def test_get_script_env_grafana_from_config(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(
|
|
inputs={'init_password.db_password': 'dbpw'},
|
|
config_data={'Monitoring': {'GRAFANA_ADMIN_PASSWORD': 'grafanapw'}},
|
|
)
|
|
env = m._get_script_env(state)
|
|
assert env['GRAFANA_ADMIN_PASSWORD'] == 'grafanapw'
|
|
|
|
def test_get_script_env_grafana_fallback(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(
|
|
inputs={'init_password.db_password': 'dbpw'},
|
|
config_data={},
|
|
)
|
|
env = m._get_script_env(state)
|
|
assert env['GRAFANA_ADMIN_PASSWORD'] == 'dbpw'
|
|
|
|
def test_get_script_env_k3s_mode(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(
|
|
inputs={
|
|
'init_cluster.cluster_env': 'service',
|
|
'init_cluster.k3s_server_url': 'myserver:6443',
|
|
'init_cluster.k3s_token': 'mytoken',
|
|
},
|
|
config_data={},
|
|
)
|
|
env = m._get_script_env(state)
|
|
assert env['PROLE_MODE'] == 'k3s'
|
|
assert env['PROLE_K3S_SERVER'] == 'https://myserver:6443'
|
|
assert env['PROLE_K3S_TOKEN'] == 'mytoken'
|
|
|
|
def test_get_script_env_argocd_from_config(self):
|
|
m = _DummyMilestone('x', 'X')
|
|
state = InstallerState(
|
|
inputs={},
|
|
config_data={'Global': {'ARGOCD_NAMESPACE': 'custom-argocd', 'REGISTRY_NAMESPACE': 'custom-reg'}},
|
|
)
|
|
env = m._get_script_env(state)
|
|
assert env['ARGOCD_NAMESPACE'] == 'custom-argocd'
|
|
assert env['REGISTRY_NAMESPACE'] == 'custom-reg'
|
|
|
|
|
|
# ===== build.py =====
|
|
|
|
class TestBuildHelpers:
|
|
def test_get_build_command(self, tmp_path):
|
|
cmd = get_build_command(tmp_path, 'dev')
|
|
assert 'cd' in cmd
|
|
assert str(tmp_path) in cmd
|
|
assert '# Dev' in cmd
|
|
|
|
def test_get_build_command_service(self, tmp_path):
|
|
cmd = get_build_command(tmp_path, 'service')
|
|
assert '# Service' in cmd
|
|
|
|
def test_get_build_command_none_env(self, tmp_path):
|
|
cmd = get_build_command(tmp_path, None)
|
|
assert '# Dev' in cmd
|
|
|
|
|
|
class TestBuildMilestone:
|
|
def test_init(self, tmp_path):
|
|
m = BuildMilestone(tmp_path, 'dev', 'next_step')
|
|
assert m.id == 'build'
|
|
assert m.title == 'Build'
|
|
assert m._next_id == 'next_step'
|
|
|
|
def test_validate_exists(self, tmp_path):
|
|
m = BuildMilestone(tmp_path)
|
|
assert m.validate(InstallerState()) is None
|
|
|
|
def test_validate_missing(self):
|
|
m = BuildMilestone(Path('/nonexistent/path'))
|
|
result = m.validate(InstallerState())
|
|
assert result is not None
|
|
assert len(result) > 0
|
|
|
|
def test_execute(self, tmp_path):
|
|
m = BuildMilestone(tmp_path, 'dev')
|
|
state = InstallerState()
|
|
msgs = []
|
|
m.execute(state, progress=lambda msg, p: msgs.append(msg))
|
|
assert 'build.command' in state.data
|
|
assert 'Build command prepared' in msgs
|
|
|
|
def test_next(self, tmp_path):
|
|
m = BuildMilestone(tmp_path, next_id='deploy')
|
|
assert m.next(InstallerState()) == 'deploy'
|
|
|
|
def test_next_none(self, tmp_path):
|
|
m = BuildMilestone(tmp_path)
|
|
assert m.next(InstallerState()) is None
|
|
|
|
|
|
# ===== ProleController =====
|
|
|
|
class TestProleController:
|
|
def test_init(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
assert c.project_root == tmp_path
|
|
assert c.verbose is False
|
|
assert c.state is not None
|
|
|
|
def test_init_verbose(self, tmp_path):
|
|
c = ProleController(tmp_path, verbose=True)
|
|
assert c.verbose is True
|
|
|
|
def test_check_docker_running(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
result = c.check_docker_running()
|
|
assert isinstance(result, bool)
|
|
|
|
def test_get_prole_db_version_defaults(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
version = c.get_prole_db_version()
|
|
assert '17.7' in version # default pg version
|
|
assert '-' in version
|
|
|
|
def test_get_prole_db_version_custom(self, tmp_path):
|
|
pg_dir = tmp_path / 'conf' / 'postgresql'
|
|
pg_dir.mkdir(parents=True)
|
|
(pg_dir / '.version').write_text('16.2')
|
|
db_dir = tmp_path / 'prole-db'
|
|
db_dir.mkdir()
|
|
(db_dir / '.version').write_text('42')
|
|
c = ProleController(tmp_path)
|
|
version = c.get_prole_db_version()
|
|
assert version == '16.2-042'
|
|
|
|
def test_run_milestones(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
m1 = _DummyMilestone('step1', 'Step 1')
|
|
m2 = _DummyMilestone('step2', 'Step 2')
|
|
msgs = []
|
|
c.run_milestones([m1, m2], progress_callback=lambda msg, p: msgs.append(msg))
|
|
assert 'step1' in c.state.completed
|
|
assert 'step2' in c.state.completed
|
|
|
|
def test_run_milestones_no_callback(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
m = _DummyMilestone('s1', 'S1')
|
|
c.run_milestones([m])
|
|
assert 's1' in c.state.completed
|
|
|
|
def test_run_script_missing(self, tmp_path):
|
|
c = ProleController(tmp_path)
|
|
# Script doesn't exist but should not crash
|
|
rc = c.run_script('nonexistent_script.sh', args=[])
|
|
# Will fail since the script doesn't exist
|
|
assert rc != 0 or rc is None
|
|
|
|
|
|
# ===== Milestone concrete subclasses (from core/milestones.py) =====
|
|
|
|
class TestDependenciesMilestone:
|
|
def test_init(self):
|
|
from installer.core.milestones import DependenciesMilestone
|
|
m = DependenciesMilestone()
|
|
assert m.id == 'dependencies'
|
|
assert m.title == 'Dependency Verification'
|
|
|
|
|
|
class TestNetworkScanMilestone:
|
|
def test_init(self):
|
|
from installer.core.milestones import NetworkScanMilestone
|
|
m = NetworkScanMilestone()
|
|
assert m.id == 'network_scan'
|
|
|
|
|
|
class TestEnvSetupMilestone:
|
|
def test_execute(self):
|
|
from installer.core.milestones import EnvSetupMilestone
|
|
m = EnvSetupMilestone()
|
|
state = InstallerState(
|
|
inputs={
|
|
'env_setup.PROLE_HOME': '/opt/prole',
|
|
'env_setup.PROLE_CONF': '/opt/prole/conf',
|
|
'env_setup.NAMESPACE': 'testns',
|
|
},
|
|
config_data={},
|
|
)
|
|
msgs = []
|
|
m.execute(state, progress=lambda msg, p: msgs.append(msg))
|
|
assert state.config_data['System Environment']['PROLE_HOME'] == '/opt/prole'
|
|
assert state.config_data['System Environment']['NAMESPACE'] == 'testns'
|
|
assert 'Environment setup complete' in msgs
|
|
|
|
|
|
class TestSecretManagementMilestone:
|
|
def test_execute_plain(self):
|
|
from installer.core.milestones import SecretManagementMilestone
|
|
m = SecretManagementMilestone()
|
|
state = InstallerState(
|
|
inputs={
|
|
'init_password.db_password': 'plaintext123',
|
|
},
|
|
)
|
|
msgs = []
|
|
m.execute(state, progress=lambda msg, p: msgs.append(msg))
|
|
assert state.inputs['init_password.db_password_confirm'] == 'plaintext123'
|
|
|
|
def test_execute_no_passwords(self):
|
|
from installer.core.milestones import SecretManagementMilestone
|
|
m = SecretManagementMilestone()
|
|
state = InstallerState(inputs={})
|
|
m.execute(state)
|
|
|
|
|
|
class TestDatabaseCreationMilestone:
|
|
def test_execute_generates_password(self):
|
|
from installer.core.milestones import DatabaseCreationMilestone
|
|
m = DatabaseCreationMilestone()
|
|
state = InstallerState(
|
|
inputs={'env_setup.NAMESPACE': 'testns'},
|
|
config_data={},
|
|
)
|
|
m.execute(state)
|
|
assert state.inputs.get('init_password.db_password')
|
|
assert len(state.inputs['init_password.db_password']) >= 20
|
|
assert state.config_data['Database Creation']['DB_NAME'] == 'testns'
|
|
|
|
def test_execute_with_password(self):
|
|
from installer.core.milestones import DatabaseCreationMilestone
|
|
m = DatabaseCreationMilestone()
|
|
state = InstallerState(
|
|
inputs={
|
|
'init_password.db_password': 'mypassword',
|
|
'env_setup.NAMESPACE': 'ns1',
|
|
},
|
|
config_data={},
|
|
)
|
|
m.execute(state)
|
|
assert state.inputs['init_password.db_password'] == 'mypassword'
|
|
|
|
def test_execute_unresolvable_openbao(self):
|
|
from installer.core.milestones import DatabaseCreationMilestone
|
|
m = DatabaseCreationMilestone()
|
|
state = InstallerState(
|
|
inputs={
|
|
'init_password.db_password': '${OPENBAO:kv/prole/ns/db#password}',
|
|
'env_setup.NAMESPACE': 'ns1',
|
|
},
|
|
config_data={},
|
|
)
|
|
m.execute(state)
|
|
pw = state.inputs['init_password.db_password']
|
|
assert not pw.startswith('${')
|
|
assert len(pw) >= 20
|
|
|
|
|
|
class TestDockerBuildMilestone:
|
|
def test_disabled(self):
|
|
from installer.core.milestones import DockerBuildMilestone
|
|
m = DockerBuildMilestone()
|
|
state = InstallerState(
|
|
inputs={'init_db_build.run_build': 'False'},
|
|
config_data={},
|
|
)
|
|
m.execute(state) # Should not raise
|
|
|
|
|
|
class TestDeploymentMilestone:
|
|
def test_init(self):
|
|
from installer.core.milestones import DeploymentMilestone
|
|
m = DeploymentMilestone()
|
|
assert m.id == 'deployment'
|