#!/usr/bin/env python3 """Isolated, non-root tests for the public MX host installer addition. No Postfix services, network DNS records or on-host config files are changed. """ import os import pathlib import re import subprocess import sys import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] INSTALLER = (ROOT / 'install-update-srs.sh').read_text() COMMON = (ROOT / 'helpers/srs-common.sh').read_text() SAMPLE = (ROOT / 'conf/srs.conf.sample').read_text() def slice_script(source, start, end): return source[source.index(start):source.index(end)] PROMPTS = slice_script(INSTALLER, 'read_config_domain_hints() {', '# Consistent installer-only confirmation prompts.') DNS = slice_script(INSTALLER, 'verify_srs_domain_dns() {', '# Compact display of APT simulations;') LOAD = slice_script(COMMON, 'load_srs_config() {', '# ----------\n# Persistent pre-SRS state') MIGRATION = re.search( r"<<'PY_MX_MIGRATE'[^\n]*\n(.*?)\nPY_MX_MIGRATE", INSTALLER, re.S ) assert MIGRATION is not None class InstallerMxHostTests(unittest.TestCase): def run_prompt(self, config=None, hostname='o13-mail.oopen.de', choices='\n\n'): with tempfile.TemporaryDirectory() as temp: conf = pathlib.Path(temp) / 'srs.conf' if config is not None: conf.write_text(config) script = f'''SRS_CONF_FILE={str(conf)!r} CONFIG_DOMAIN_HINT='' CONFIG_MX_HINT='' SELECTED_MX_HOST='' SELECTED_SRS_DOMAIN='' hostname() {{ printf '%s\\n' {hostname!r}; }} {PROMPTS} prompt_srs_domain printf 'RESULT_MX=%s\\nRESULT_SRS=%s\\n' "$SELECTED_MX_HOST" "$SELECTED_SRS_DOMAIN" ''' return subprocess.run( ['bash', '-c', script], input=choices, text=True, capture_output=True ) def test_first_install_defaults_from_hostname(self): result = self.run_prompt() self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('RESULT_MX=o13-mail.oopen.de', result.stdout) self.assertIn('RESULT_SRS=srs.o13-mail.oopen.de', result.stdout) def test_public_mx_override_changes_srs_suggestion(self): result = self.run_prompt(choices='mail.interventionistische-linke.org\n\n') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('RESULT_MX=mail.interventionistische-linke.org', result.stdout) self.assertIn('RESULT_SRS=srs.mail.interventionistische-linke.org', result.stdout) def test_configured_values_take_precedence(self): config = '_MX_HOST="mail.example.org"\n_SRS_DOMAIN="srs.mail.example.org"\n' result = self.run_prompt(config=config) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('RESULT_MX=mail.example.org', result.stdout) self.assertIn('RESULT_SRS=srs.mail.example.org', result.stdout) def test_legacy_srs_domain_preserved(self): config = '_SRS_DOMAIN="srs.original.example.org"\n' result = self.run_prompt(config=config, choices='mail.example.org\n\n') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('RESULT_MX=mail.example.org', result.stdout) self.assertIn('RESULT_SRS=srs.original.example.org', result.stdout) def test_invalid_host_reprompted(self): result = self.run_prompt(choices='bad_host\nmail.example.org\n\n') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('[ invalid ]', result.stderr) self.assertIn('RESULT_MX=mail.example.org', result.stdout) def test_prompt_eof_fails_without_defaulting(self): result = self.run_prompt(choices='') self.assertEqual(result.returncode, 2) def test_dns_preflight_passes_selected_mx_not_postfix_myhostname(self): with tempfile.TemporaryDirectory() as temp: binpath = pathlib.Path(temp) (binpath / 'dig').write_text('#!/bin/sh\nexit 0\n') (binpath / 'dig').chmod(0o755) postconf = binpath / 'postconf' postconf.write_text('''#!/bin/sh case "$2" in smtp_bind_address) echo 83.223.90.231 ;; smtp_bind_address6) echo 2a01:31:0:2::ad52:a231 ;; *) echo 'unexpected postconf query' >&2; exit 4 ;; esac ''') postconf.chmod(0o755) checker = binpath / 'check.py' checker.write_text('''import sys print('ARGS=' + ';'.join(sys.argv[1:])) ''') script = f'''SELECTED_MX_HOST=mail.interventionistische-linke.org SELECTED_SRS_DOMAIN=srs.mail.interventionistische-linke.org SRS_DNS_CHECKER={str(checker)!r} {DNS} verify_srs_domain_dns ''' env = dict(os.environ, PATH=f'{temp}:' + os.environ['PATH']) proc = subprocess.run(['bash','-c',script], capture_output=True, text=True, env=env) self.assertEqual(proc.returncode, 0, proc.stderr + proc.stdout) self.assertIn('--hostname;mail.interventionistische-linke.org', proc.stdout) self.assertIn('--ipv4;83.223.90.231', proc.stdout) self.assertIn('--ipv6;2a01:31:0:2::ad52:a231', proc.stdout) def test_existing_config_migration_is_append_only(self): with tempfile.TemporaryDirectory() as tmp: src = pathlib.Path(tmp) / 'old.conf' dst = pathlib.Path(tmp) / 'new.conf' content = b'# existing comments\n_SRS_DOMAIN="srs.example.org"\n\n' src.write_bytes(content) proc = subprocess.run([sys.executable, '-', str(src), str(dst), 'mail.example.org'], input=MIGRATION.group(1), capture_output=True, text=True) self.assertEqual(proc.returncode, 0, proc.stderr) self.assertTrue(dst.read_bytes().startswith(content)) self.assertIn(b'_MX_HOST="mail.example.org"\n', dst.read_bytes()) self.assertEqual(src.read_bytes(), content) def test_migration_rejects_existing_mx_assignment(self): with tempfile.TemporaryDirectory() as tmp: src = pathlib.Path(tmp) / 'old.conf' dst = pathlib.Path(tmp) / 'new.conf' src.write_text('_MX_HOST="other.example.org"\n') proc = subprocess.run([sys.executable, '-', str(src), str(dst), 'mail.example.org'], input=MIGRATION.group(1), capture_output=True, text=True) self.assertNotEqual(proc.returncode, 0) self.assertFalse(dst.exists()) def run_config_load(self, content): with tempfile.TemporaryDirectory() as tmp: conf = pathlib.Path(tmp) / 'srs.conf' conf.write_text(content) script = f'''SRS_CONF_FILE={str(conf)!r} SRS_CONF_SAMPLE_FILE=unused check_secure_source_file() {{ :; }} echononl() {{ :; }} echo_failed() {{ :; }} echo_ok() {{ :; }} fatal() {{ echo "FATAL: $*" >&2; exit 1; }} {LOAD} load_srs_config printf 'MX_IS_SET=%s\\n' "${{_MX_HOST+x}}" ''' return subprocess.run(['bash','-c',script],text=True,capture_output=True) def test_load_config_accepts_legacy_without_mx_host(self): result = self.run_config_load('_SRS_DOMAIN="srs.example.org"\n') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('MX_IS_SET=', result.stdout) def test_load_config_accepts_valid_mx_host(self): result = self.run_config_load('_MX_HOST="mail.example.org"\n_SRS_DOMAIN="srs.example.org"\n') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn('MX_IS_SET=x', result.stdout) def test_load_config_rejects_invalid_mx_host(self): result = self.run_config_load('_MX_HOST="bad_host"\n_SRS_DOMAIN="srs.example.org"\n') self.assertEqual(result.returncode, 1) self.assertIn('Invalid public MX hostname', result.stderr) def test_templates_and_security_contract(self): self.assertIn('_MX_HOST=""', SAMPLE) self.assertIn('_SRS_DOMAIN=""', SAMPLE) self.assertIn('unset _MX_HOST', COMMON) self.assertIn('if [[ -n "${_MX_HOST+x}" ]]', COMMON) self.assertIn('"${_MX_HOST,,}" != "$SELECTED_MX_HOST"', INSTALLER) self.assertIn('install_regular_file "${STAGE_DIR}/rendered/srs.conf" "$SRS_CONF_FILE"', INSTALLER) if __name__ == '__main__': unittest.main(verbosity=2)