prole/tests/installer/test_core_classes.py

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'