Archive discontinued selective SRS implementation
This commit is contained in:
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline, path-relocated installer contract tests. All external commands mocked.
|
||||
Do not equate these tests with genuine Postfix/PostSRSd integration.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os, subprocess, tempfile, zipfile, re, sys, shutil
|
||||
|
||||
SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
|
||||
|
||||
def putexe(file, content):
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
file.write_text('#!/usr/bin/env bash\n' + content)
|
||||
file.chmod(0o755)
|
||||
|
||||
def setup(root):
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
repo = root / 'repo'
|
||||
repo.mkdir()
|
||||
if SOURCE.is_file():
|
||||
with zipfile.ZipFile(SOURCE) as z:
|
||||
z.extractall(repo)
|
||||
else:
|
||||
for source_file in SOURCE.rglob('*'):
|
||||
if source_file.is_file():
|
||||
dst = repo / source_file.relative_to(SOURCE)
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source_file, dst)
|
||||
for path in (repo/'install-update-srs.sh', repo/'helpers/srs-common.sh',
|
||||
*list((repo/'files/srs').glob('*.py')),
|
||||
*list((repo/'files/srs/templates').glob('*'))):
|
||||
s = path.read_text()
|
||||
# Keep all managed install/config/queue paths in an unprivileged fixture tree.
|
||||
# The postconf absolute command must also be relocated.
|
||||
s = s.replace('/usr/sbin/postconf', str(root/'bin/postconf'))
|
||||
for prefix in ['/etc/', '/usr/local/', '/var/', '/run/lock/', '/root/srs-installer-backups']:
|
||||
new = str(root) + prefix
|
||||
s = s.replace(prefix, new)
|
||||
# prevent reading the real original root Postfix config in our fixtures
|
||||
path.write_text(s)
|
||||
(repo/'install-update-srs.sh').chmod(0o755)
|
||||
for p in ['etc/postfix','usr/local/sbin','run/lock','var/spool','var/lib','etc/systemd/system','flags']:
|
||||
(root/p).mkdir(parents=True, exist_ok=True)
|
||||
(root/'etc/postfix/main.cf').write_text('myhostname = mail.example.org\n')
|
||||
(root/'etc/postfix/master.cf').write_text('smtp inet n - y - - smtpd\nlocalhost:10025 inet n - y - - smtpd\n -o receive_override_options=no_unknown_recipient_checks\n')
|
||||
(repo/'conf').mkdir(exist_ok=True)
|
||||
# All expected lookup paths are extracted from the (relocated) reference source.
|
||||
alias = (repo/'files/srs/srs-alias-decision.py').read_text()
|
||||
for typ,name in set(re.findall(r'"(pgsql|btree):([^" ]+)"', alias)):
|
||||
f = Path(name + ('.db' if typ == 'btree' else ''))
|
||||
f.parent.mkdir(parents=True,exist_ok=True)
|
||||
f.touch()
|
||||
bin = root/'bin'
|
||||
bin.mkdir()
|
||||
putexe(bin/'postconf', '''
|
||||
name="${@: -1}"
|
||||
# e.mx.oopen.de has two pre-existing unrelated warnings in main.cf.
|
||||
# They are emitted on every postconf call while the requested value is valid.
|
||||
echo "postconf: warning: ${SRS_TEST_ROOT}/etc/postfix/main.cf: unused parameter: policyd-spf_time_limit=3600" >&2
|
||||
# The warning path matches the source main.cf in the relocated fixture.
|
||||
# Existing warnings must be distinguished from queried-param errors.
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/unknown_parameter_warning" ]]; then
|
||||
echo "postconf: warning: smtp_tls_cafile: unknown parameter" >&2
|
||||
fi
|
||||
case "$name" in
|
||||
myhostname) echo mail.example.org;;
|
||||
mydomain) echo example.org;;
|
||||
myorigin) echo /etc/mailname;;
|
||||
mydestination) echo 'mail.example.org localhost';;
|
||||
relay_domains) echo '\u0024mydestination';;
|
||||
virtual_alias_maps) echo 'pgsql:some-alias';;
|
||||
virtual_alias_domains) echo 'btree:some-domain';;
|
||||
virtual_mailbox_maps) echo 'pgsql:some-mailbox';;
|
||||
virtual_mailbox_domains) echo 'pgsql:some-domain';;
|
||||
smtp_bind_address) echo 192.0.2.22;;
|
||||
smtp_bind_address6) echo 2001:db8::22;;
|
||||
smtp_dns_support_level) echo dnssec;;
|
||||
smtp_host_lookup) echo dns;;
|
||||
smtp_tls_CAfile) echo /etc/ssl/certs/ca-certificates.crt;;
|
||||
smtp_tls_mandatory_protocols|smtp_tls_protocols) echo '>=TLSv1.2';;
|
||||
smtp_tls_security_level) echo dane;;
|
||||
*) echo "unknown $name" >&2; exit 1;;
|
||||
esac
|
||||
''')
|
||||
putexe(bin/'systemctl', '''
|
||||
if [[ "$1" == is-active ]]; then exit 3; fi
|
||||
exit 0
|
||||
''')
|
||||
putexe(bin/'git', '''
|
||||
if [[ "$1" == clone ]]; then
|
||||
mkdir -p "${@: -1}"; exit 0
|
||||
fi
|
||||
exec /usr/bin/git "$@"
|
||||
''')
|
||||
putexe(bin/'cmake', '''
|
||||
if [[ "$1" == --version ]]; then echo 'cmake version 3.31.6'; exit 0; fi
|
||||
if [[ "$1" == -S ]]; then
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
arg="${!i}"
|
||||
if [[ "$arg" == -B ]]; then n=$((i+1)); build="${!n}"; fi
|
||||
if [[ "$arg" == -DCMAKE_INSTALL_PREFIX=* ]]; then prefix="${arg#*=}"; fi
|
||||
done
|
||||
mkdir -p "$build"; printf '%s\\n' "$prefix" > "$build/prefix"; exit 0
|
||||
fi
|
||||
if [[ "$1" == --build ]]; then exit 0; fi
|
||||
if [[ "$1" == --install ]]; then
|
||||
prefix="$(cat "$2/prefix")"
|
||||
mkdir -p "${DESTDIR}${prefix}/sbin"
|
||||
printf '#!/bin/sh\\nexit 0\\n' > "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||
chmod +x "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||
exit 0
|
||||
fi
|
||||
exit 9
|
||||
''')
|
||||
putexe(bin/'ctest','exit 0\n')
|
||||
putexe(bin/'dig','exit 0\n')
|
||||
putexe(bin/'postmap','''
|
||||
case "$1" in
|
||||
btree:*) dest="${1#btree:}"; touch "${dest}.db";;
|
||||
*) exit 1;;
|
||||
esac
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postmap" ]]; then exit 1; fi
|
||||
exit 0
|
||||
''')
|
||||
putexe(bin/'postfix','''
|
||||
if [[ "$*" == *check* ]]; then
|
||||
mkdir -p "${SRS_TEST_ROOT}/var/spool/postfix-srs-delivery/private" "${SRS_TEST_ROOT}/var/lib/postfix-srs-delivery"
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postfix" ]]; then echo 'forced postfix check error' >&2; exit 1; fi
|
||||
fi
|
||||
exit 0
|
||||
''')
|
||||
(repo/'helpers/srs-dns-check.py').write_text('print(" Simulated authoritative DNS: OK")\n')
|
||||
return repo
|
||||
|
||||
def run(root, repo, *, fail=None, version=None):
|
||||
if fail: (root/'flags'/('fail_'+fail)).touch()
|
||||
cmd = f"{repo/'install-update-srs.sh'} --install"
|
||||
if version: cmd += f' --version {version}'
|
||||
env = os.environ.copy()
|
||||
env.update({'PATH':str(root/'bin') + ':' + env['PATH'], 'SRS_TEST_ROOT':str(root), 'LC_ALL':'C'})
|
||||
# Running inside util-linux script supplies a real PTY for init_srs_runtime.
|
||||
result = subprocess.run(['script','-q','-e','-c',cmd,'/dev/null'], input='srs.example.org\nyes\nyes\n',
|
||||
text=True, capture_output=True, timeout=35, env=env)
|
||||
return result
|
||||
|
||||
def assert_ok(condition, msg):
|
||||
if not condition: raise AssertionError(msg)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='oopen-srs-test-') as tmp:
|
||||
base=Path(tmp)
|
||||
os.chmod(base, 0o2770) # Deliberately inherit SGID on fixture subdirectories.
|
||||
root=base/'initial'
|
||||
repo=setup(root)
|
||||
result=run(root,repo)
|
||||
print('initial install', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'initial installation failed')
|
||||
assert_ok((root/'usr/local/postsrsd').is_symlink(), 'missing symlink')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').exists(), 'missing srs forward')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').stat().st_mode & 0o777 == 0o600, 'secret mode')
|
||||
assert_ok('smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt' in (root/'etc/postfix-srs/delivery/main.cf').read_text(),'rendered TLS CA')
|
||||
print(" secret dir mode:", oct((root/"etc/postsrsd-srs").stat().st_mode & 0o777), "uid", (root/"etc/postsrsd-srs").stat().st_uid)
|
||||
print(" stat check:", subprocess.check_output(["stat", "-c", "%u:%g:%a", str(root/"etc/postsrsd-srs")], text=True).strip())
|
||||
print(' initial install: state, secret, config, symlink, CAfile verified')
|
||||
original_secret=(root/'etc/postsrsd-srs/secret').read_bytes()
|
||||
original_forward=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||
original_link=os.readlink(root/'usr/local/postsrsd')
|
||||
result=run(root,repo)
|
||||
print('maintenance update', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'maintenance update failed')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'secret changed')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'link changed')
|
||||
print(' maintenance update: original secret and binary retained')
|
||||
# Force late failure, after Postfix configuration and all units have been deployed.
|
||||
source=repo/'files/srs/srs-forward.py'
|
||||
source.write_text(source.read_text()+'\n# injected test change\n')
|
||||
result=run(root,repo,fail='postfix')
|
||||
print('failed maintenance rollback', result.returncode)
|
||||
if result.returncode==0: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes()==original_forward,'rollback did not restore file')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'rollback changed secret')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'rollback changed symlink')
|
||||
print(' failure rollback: file restored; secret and symlink preserved')
|
||||
(root/'flags/fail_postfix').unlink()
|
||||
result=run(root,repo,version='2.4.2')
|
||||
print('version upgrade', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'version upgrade failed')
|
||||
assert_ok((root/'usr/local/postsrsd-2.4.1').exists(),'prior prefix missing')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','new link incorrect')
|
||||
print(' version upgrade: previous prefix retained; link updated')
|
||||
(root/'flags/fail_postfix').touch()
|
||||
result=run(root,repo,version='2.4.3')
|
||||
print('failed version upgrade rollback', result.returncode)
|
||||
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','previous version link not restored')
|
||||
assert_ok(not (root/'usr/local/postsrsd-2.4.3').exists(),'failed version prefix not removed')
|
||||
print(' failed version upgrade: symlink restored, new prefix removed')
|
||||
|
||||
# Active SRS configuration must prohibit any maintenance update before writes.
|
||||
(root/'flags/fail_postfix').unlink()
|
||||
active_marker = '# BEGIN OOPEN-SRS-MANAGED'
|
||||
existing_main=(root/'etc/postfix/main.cf')
|
||||
original_main=existing_main.read_text()
|
||||
existing_main.write_text(original_main + active_marker + '\n')
|
||||
before=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||
result=run(root,repo,version='2.4.2')
|
||||
print('active integration guard', result.returncode)
|
||||
assert_ok(result.returncode != 0,'active SRS was not refused')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes() == before, 'active check changed scripts')
|
||||
existing_main.write_text(original_main)
|
||||
print(' active integration guard: blocked before file changes')
|
||||
|
||||
# Independent initial installation with late failure. New SRS files must be removed.
|
||||
root=base/'failed-first'
|
||||
repo=setup(root)
|
||||
result=run(root,repo,fail='postfix')
|
||||
print('failed first install', result.returncode)
|
||||
assert_ok(result.returncode != 0,'injected first-install failure not detected')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'first-install rollback left symlink')
|
||||
assert_ok(not (root/'usr/local/postsrsd-2.4.1').exists(), 'first-install rollback left prefix')
|
||||
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(), 'first-install rollback left secret')
|
||||
assert_ok(not (root/'etc/postfix-srs/delivery/main.cf').exists(), 'first-install rollback left config')
|
||||
assert_ok((root/'var/spool/postfix-srs-delivery/private').is_dir(), 'test Postfix runtime queue unexpectedly deleted')
|
||||
print(' failed first install: tracked files removed; Postfix runtime directory safely preserved')
|
||||
|
||||
# Fail at postmap, before systemd and Postfix checks.
|
||||
root=base/'failed-map'
|
||||
repo=setup(root)
|
||||
result=run(root,repo,fail='postmap')
|
||||
print('failed postmap rollback', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'injected postmap failure not detected')
|
||||
assert_ok(not (root/'etc/postfix-srs/delivery/transport.db').exists(),'failed postmap index remains')
|
||||
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(),'failed postmap secret remains')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(),'failed postmap link remains')
|
||||
print(' failed postmap: generated index and secret removed')
|
||||
|
||||
# Unexpected warnings must never be silently swallowed.
|
||||
root=base/'unexpected-warning'
|
||||
repo=setup(root)
|
||||
(root/'flags/unknown_parameter_warning').touch()
|
||||
result=run(root, repo)
|
||||
print('unexpected postconf warning', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'unexpected postconf warning accepted')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'unexpected warning allowed installation')
|
||||
print(' unrelated existing warnings tolerated, queried-parameter warning rejected')
|
||||
|
||||
# A template referencing an unknown Postfix parameter must fail closed.
|
||||
root=base/'bad-template-parameter'
|
||||
repo=setup(root)
|
||||
template=root/'repo/files/srs/templates/delivery-main.cf.in'
|
||||
contents=template.read_text()
|
||||
assert_ok('smtp_tls_CAfile = @SMTP_TLS_CAFILE@' in contents, 'missing expected template parameter')
|
||||
template.write_text(contents.replace('smtp_tls_CAfile = @SMTP_TLS_CAFILE@',
|
||||
'smtp_tls_cafile = @SMTP_TLS_CAFILE@'))
|
||||
result=run(root, repo)
|
||||
print('invalid template parameter', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'unknown template Postfix parameter accepted')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'invalid template allowed installation')
|
||||
print(' unknown requested Postfix parameter rejected')
|
||||
|
||||
print('ALL OFFLINE MOCK CONTRACT TESTS PASSED')
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline, path-relocated installer contract tests. All external commands mocked.
|
||||
Do not equate these tests with genuine Postfix/PostSRSd integration.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os, subprocess, tempfile, re, sys, shutil, signal, types
|
||||
|
||||
SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
|
||||
|
||||
def putexe(file, content):
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
file.write_text('#!/usr/bin/env bash\n' + content)
|
||||
file.chmod(0o755)
|
||||
|
||||
def setup(root):
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
repo = root / 'repo'
|
||||
repo.mkdir()
|
||||
# Copy only public SRS source files needed by the test. Never copy the
|
||||
# complete mailserver repository, .git, credentials, local conf/srs.conf,
|
||||
# or unrelated applications into an executable test fixture.
|
||||
if not SOURCE.is_dir():
|
||||
raise RuntimeError(f'SOURCE must be a repository directory: {SOURCE}')
|
||||
required = [
|
||||
'install-update-srs.sh',
|
||||
'helpers/srs-common.sh',
|
||||
'helpers/srs-dns-check.py',
|
||||
'helpers/srs-virtual-alias.py',
|
||||
'files/srs/srs-alias-decision.py',
|
||||
'files/srs/srs-forward.py',
|
||||
'files/srs/srs-reverse-map.py',
|
||||
'files/srs/srs-reverse-policy.py',
|
||||
]
|
||||
templates = SOURCE/'files/srs/templates'
|
||||
required += [str(f.relative_to(SOURCE)) for f in templates.iterdir() if f.is_file() and not f.is_symlink()]
|
||||
for rel in required:
|
||||
src = SOURCE/rel
|
||||
if not src.is_file() or src.is_symlink():
|
||||
raise RuntimeError(f'Missing or symlinked source file: {src}')
|
||||
dst = repo/rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
for path in (repo/'install-update-srs.sh', repo/'helpers/srs-common.sh',
|
||||
*list((repo/'files/srs').glob('*.py')),
|
||||
*list((repo/'files/srs/templates').glob('*'))):
|
||||
s = path.read_text()
|
||||
# Keep all managed install/config/queue paths in an unprivileged fixture tree.
|
||||
# The postconf absolute command must also be relocated.
|
||||
s = s.replace('/usr/sbin/postconf', str(root/'bin/postconf'))
|
||||
for prefix in ['/etc/', '/usr/local/', '/var/', '/run/lock/', '/root/srs-installer-backups']:
|
||||
new = str(root) + prefix
|
||||
s = s.replace(prefix, new)
|
||||
# prevent reading the real original root Postfix config in our fixtures
|
||||
if path == repo/'helpers/srs-common.sh':
|
||||
# The test runner uses a pipe for scripted confirmations;
|
||||
# allow that only inside the copied, isolated helper fixture.
|
||||
needle = 'if [[ -t 1 ]]; then'
|
||||
if s.count(needle) != 1:
|
||||
raise RuntimeError('Unexpected terminal gate in helper')
|
||||
s = s.replace(needle, 'if true; then', 1)
|
||||
path.write_text(s)
|
||||
(repo/'install-update-srs.sh').chmod(0o755)
|
||||
for p in ['etc/postfix','usr/local/sbin','run/lock','var/spool','var/lib','etc/systemd/system','flags']:
|
||||
(root/p).mkdir(parents=True, exist_ok=True)
|
||||
(root/'etc/postfix/main.cf').write_text('myhostname = mail.example.org\n')
|
||||
(root/'etc/postfix/master.cf').write_text('smtp inet n - y - - smtpd\nlocalhost:10025 inet n - y - - smtpd\n -o receive_override_options=no_unknown_recipient_checks\n')
|
||||
(repo/'conf').mkdir(exist_ok=True)
|
||||
# All expected lookup paths are extracted from the (relocated) reference source.
|
||||
alias = (repo/'files/srs/srs-alias-decision.py').read_text()
|
||||
for typ,name in set(re.findall(r'"(pgsql|btree):([^" ]+)"', alias)):
|
||||
f = Path(name + ('.db' if typ == 'btree' else ''))
|
||||
f.parent.mkdir(parents=True,exist_ok=True)
|
||||
f.touch()
|
||||
bin = root/'bin'
|
||||
bin.mkdir()
|
||||
putexe(bin/'postconf', '''
|
||||
name="${@: -1}"
|
||||
# e.mx.oopen.de has two pre-existing unrelated warnings in main.cf.
|
||||
# They are emitted on every postconf call while the requested value is valid.
|
||||
echo "postconf: warning: ${SRS_TEST_ROOT}/etc/postfix/main.cf: unused parameter: policyd-spf_time_limit=3600" >&2
|
||||
# The warning path matches the source main.cf in the relocated fixture.
|
||||
# Existing warnings must be distinguished from queried-param errors.
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/unknown_parameter_warning" ]]; then
|
||||
echo "postconf: warning: smtp_tls_cafile: unknown parameter" >&2
|
||||
fi
|
||||
case "$name" in
|
||||
mail_owner) echo postfix;;
|
||||
myhostname) echo mail.example.org;;
|
||||
mydomain) echo example.org;;
|
||||
myorigin) echo /etc/mailname;;
|
||||
mydestination) echo 'mail.example.org localhost';;
|
||||
relay_domains) echo '\u0024mydestination';;
|
||||
virtual_alias_maps) echo 'pgsql:some-alias';;
|
||||
virtual_alias_domains) echo 'btree:some-domain';;
|
||||
virtual_mailbox_maps) echo 'pgsql:some-mailbox';;
|
||||
virtual_mailbox_domains) echo 'pgsql:some-domain';;
|
||||
smtp_bind_address) echo 192.0.2.22;;
|
||||
smtp_bind_address6) echo 2001:db8::22;;
|
||||
smtp_dns_support_level) echo dnssec;;
|
||||
smtp_host_lookup) echo dns;;
|
||||
smtp_tls_CAfile) echo /etc/ssl/certs/ca-certificates.crt;;
|
||||
smtp_tls_mandatory_protocols|smtp_tls_protocols) echo '>=TLSv1.2';;
|
||||
smtp_tls_security_level) echo dane;;
|
||||
*) echo "unknown $name" >&2; exit 1;;
|
||||
esac
|
||||
''')
|
||||
putexe(bin/'id', '''
|
||||
if [[ "$1" == -u && "$2" == postfix ]]; then echo 123; exit 0; fi
|
||||
if [[ "$1" == -g && "$2" == postfix ]]; then echo 123; exit 0; fi
|
||||
exec /usr/bin/id "$@"
|
||||
''')
|
||||
putexe(bin/'systemctl', '''
|
||||
if [[ "$1" == is-active ]]; then exit 3; fi
|
||||
exit 0
|
||||
''')
|
||||
putexe(bin/'git', '''
|
||||
if [[ "$1" == clone ]]; then
|
||||
mkdir -p "${@: -1}"; exit 0
|
||||
fi
|
||||
exec /usr/bin/git "$@"
|
||||
''')
|
||||
putexe(bin/'cmake', '''
|
||||
if [[ "$1" == --version ]]; then echo 'cmake version 3.31.6'; exit 0; fi
|
||||
if [[ "$1" == -S ]]; then
|
||||
for ((i=1; i<=$#; i++)); do
|
||||
arg="${!i}"
|
||||
if [[ "$arg" == -B ]]; then n=$((i+1)); build="${!n}"; fi
|
||||
if [[ "$arg" == -DCMAKE_INSTALL_PREFIX=* ]]; then prefix="${arg#*=}"; fi
|
||||
done
|
||||
mkdir -p "$build"; printf '%s\\n' "$prefix" > "$build/prefix"; exit 0
|
||||
fi
|
||||
if [[ "$1" == --build ]]; then exit 0; fi
|
||||
if [[ "$1" == --install ]]; then
|
||||
prefix="$(cat "$2/prefix")"
|
||||
mkdir -p "${DESTDIR}${prefix}/sbin"
|
||||
printf '#!/bin/sh\\nexit 0\\n' > "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||
chmod +x "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||
exit 0
|
||||
fi
|
||||
exit 9
|
||||
''')
|
||||
putexe(bin/'ctest','exit 0\n')
|
||||
putexe(bin/'dig','exit 0\n')
|
||||
putexe(bin/'postmap','''
|
||||
case "$1" in
|
||||
btree:*) dest="${1#btree:}"; touch "${dest}.db";;
|
||||
*) exit 1;;
|
||||
esac
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postmap" ]]; then exit 1; fi
|
||||
exit 0
|
||||
''')
|
||||
putexe(bin/'postfix','''
|
||||
if [[ "$*" == *check* ]]; then
|
||||
mkdir -p "${SRS_TEST_ROOT}/var/spool/postfix-srs-delivery/private" "${SRS_TEST_ROOT}/var/lib/postfix-srs-delivery"
|
||||
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postfix" ]]; then echo 'forced postfix check error' >&2; exit 1; fi
|
||||
fi
|
||||
exit 0
|
||||
''')
|
||||
(repo/'helpers/srs-dns-check.py').write_text('print(" Simulated authoritative DNS: OK")\n')
|
||||
return repo
|
||||
|
||||
def run(root, repo, *, fail=None, version=None):
|
||||
if fail: (root/'flags'/('fail_'+fail)).touch()
|
||||
argv = [str(repo/'install-update-srs.sh'), '--install']
|
||||
if version: argv.extend(['--version', version])
|
||||
env = os.environ.copy()
|
||||
env.update({'PATH':str(root/'bin') + ':' + env['PATH'], 'SRS_TEST_ROOT':str(root), 'LC_ALL':'C'})
|
||||
# No real PTY is needed: the terminal requirement is disabled only in the
|
||||
# relocated helper copy, never in the actual repository.
|
||||
# File-backed capture prevents subprocess pipe hangs from descendant
|
||||
# processes retaining output fds. A timeout kills the entire fixture group.
|
||||
with tempfile.TemporaryFile(mode='w+t') as out:
|
||||
proc = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=out,
|
||||
stderr=subprocess.STDOUT, text=True, env=env,
|
||||
start_new_session=True)
|
||||
try:
|
||||
proc.communicate(input='srs.example.org\nyes\nyes\n', timeout=20)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait(timeout=5)
|
||||
raise RuntimeError('Mock installer timed out')
|
||||
out.seek(0)
|
||||
return types.SimpleNamespace(returncode=proc.returncode, stdout=out.read())
|
||||
|
||||
def assert_ok(condition, msg):
|
||||
if not condition: raise AssertionError(msg)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='oopen-srs-test-') as tmp:
|
||||
base=Path(tmp)
|
||||
os.chmod(base, 0o2700) # Test SGID inheritance without granting group access.
|
||||
root=base/'initial'
|
||||
repo=setup(root)
|
||||
result=run(root,repo)
|
||||
print('initial install', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'initial installation failed')
|
||||
assert_ok((root/'usr/local/postsrsd').is_symlink(), 'missing symlink')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').exists(), 'missing srs forward')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').stat().st_mode & 0o777 == 0o600, 'secret mode')
|
||||
assert_ok('smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt' in (root/'etc/postfix-srs/delivery/main.cf').read_text(),'rendered TLS CA')
|
||||
print(" secret dir mode:", oct((root/"etc/postsrsd-srs").stat().st_mode & 0o777), "uid", (root/"etc/postsrsd-srs").stat().st_uid)
|
||||
print(" stat check:", subprocess.check_output(["stat", "-c", "%u:%g:%a", str(root/"etc/postsrsd-srs")], text=True).strip())
|
||||
print(' initial install: state, secret, config, symlink, CAfile verified')
|
||||
original_secret=(root/'etc/postsrsd-srs/secret').read_bytes()
|
||||
original_forward=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||
original_link=os.readlink(root/'usr/local/postsrsd')
|
||||
result=run(root,repo)
|
||||
print('maintenance update', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'maintenance update failed')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'secret changed')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'link changed')
|
||||
print(' maintenance update: original secret and binary retained')
|
||||
# Force late failure, after Postfix configuration and all units have been deployed.
|
||||
source=repo/'files/srs/srs-forward.py'
|
||||
source.write_text(source.read_text()+'\n# injected test change\n')
|
||||
result=run(root,repo,fail='postfix')
|
||||
print('failed maintenance rollback', result.returncode)
|
||||
if result.returncode==0: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes()==original_forward,'rollback did not restore file')
|
||||
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'rollback changed secret')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'rollback changed symlink')
|
||||
print(' failure rollback: file restored; secret and symlink preserved')
|
||||
(root/'flags/fail_postfix').unlink()
|
||||
result=run(root,repo,version='2.4.2')
|
||||
print('version upgrade', result.returncode)
|
||||
if result.returncode: print(result.stdout[-2500:])
|
||||
assert_ok(result.returncode == 0,'version upgrade failed')
|
||||
assert_ok((root/'usr/local/postsrsd-2.4.1').exists(),'prior prefix missing')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','new link incorrect')
|
||||
print(' version upgrade: previous prefix retained; link updated')
|
||||
(root/'flags/fail_postfix').touch()
|
||||
result=run(root,repo,version='2.4.3')
|
||||
print('failed version upgrade rollback', result.returncode)
|
||||
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','previous version link not restored')
|
||||
assert_ok(not (root/'usr/local/postsrsd-2.4.3').exists(),'failed version prefix not removed')
|
||||
print(' failed version upgrade: symlink restored, new prefix removed')
|
||||
|
||||
# Active SRS configuration must prohibit any maintenance update before writes.
|
||||
(root/'flags/fail_postfix').unlink()
|
||||
active_marker = '# BEGIN OOPEN-SRS-MANAGED'
|
||||
existing_main=(root/'etc/postfix/main.cf')
|
||||
original_main=existing_main.read_text()
|
||||
existing_main.write_text(original_main + active_marker + '\n')
|
||||
before=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||
result=run(root,repo,version='2.4.2')
|
||||
print('active integration guard', result.returncode)
|
||||
assert_ok(result.returncode != 0,'active SRS was not refused')
|
||||
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes() == before, 'active check changed scripts')
|
||||
existing_main.write_text(original_main)
|
||||
print(' active integration guard: blocked before file changes')
|
||||
|
||||
# Independent initial installation with late failure. New SRS files must be removed.
|
||||
root=base/'failed-first'
|
||||
repo=setup(root)
|
||||
result=run(root,repo,fail='postfix')
|
||||
print('failed first install', result.returncode)
|
||||
assert_ok(result.returncode != 0,'injected first-install failure not detected')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'first-install rollback left symlink')
|
||||
assert_ok(not (root/'usr/local/postsrsd-2.4.1').exists(), 'first-install rollback left prefix')
|
||||
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(), 'first-install rollback left secret')
|
||||
assert_ok(not (root/'etc/postfix-srs/delivery/main.cf').exists(), 'first-install rollback left config')
|
||||
assert_ok((root/'var/spool/postfix-srs-delivery/private').is_dir(), 'test Postfix runtime queue unexpectedly deleted')
|
||||
print(' failed first install: tracked files removed; Postfix runtime directory safely preserved')
|
||||
|
||||
# Fail at postmap, before systemd and Postfix checks.
|
||||
root=base/'failed-map'
|
||||
repo=setup(root)
|
||||
result=run(root,repo,fail='postmap')
|
||||
print('failed postmap rollback', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'injected postmap failure not detected')
|
||||
assert_ok(not (root/'etc/postfix-srs/delivery/transport.db').exists(),'failed postmap index remains')
|
||||
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(),'failed postmap secret remains')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(),'failed postmap link remains')
|
||||
print(' failed postmap: generated index and secret removed')
|
||||
|
||||
# Unexpected warnings must never be silently swallowed.
|
||||
root=base/'unexpected-warning'
|
||||
repo=setup(root)
|
||||
(root/'flags/unknown_parameter_warning').touch()
|
||||
result=run(root, repo)
|
||||
print('unexpected postconf warning', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'unexpected postconf warning accepted')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'unexpected warning allowed installation')
|
||||
print(' unrelated existing warnings tolerated, queried-parameter warning rejected')
|
||||
|
||||
# A template referencing an unknown Postfix parameter must fail closed.
|
||||
root=base/'bad-template-parameter'
|
||||
repo=setup(root)
|
||||
template=root/'repo/files/srs/templates/delivery-main.cf.in'
|
||||
contents=template.read_text()
|
||||
assert_ok('smtp_tls_CAfile = @SMTP_TLS_CAFILE@' in contents, 'missing expected template parameter')
|
||||
template.write_text(contents.replace('smtp_tls_CAfile = @SMTP_TLS_CAFILE@',
|
||||
'smtp_tls_cafile = @SMTP_TLS_CAFILE@'))
|
||||
result=run(root, repo)
|
||||
print('invalid template parameter', result.returncode)
|
||||
assert_ok(result.returncode != 0, 'unknown template Postfix parameter accepted')
|
||||
assert_ok(not (root/'usr/local/postsrsd').exists(), 'invalid template allowed installation')
|
||||
print(' unknown requested Postfix parameter rejected')
|
||||
|
||||
print('ALL OFFLINE MOCK CONTRACT TESTS PASSED')
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline regression tests for the selective SRS DNS preflight helper.
|
||||
|
||||
All DNS responses are mocked. No live DNS queries, Postfix changes,
|
||||
or SRS installation operations are performed.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import runpy
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HELPER = ROOT / "helpers/srs-dns-check.py"
|
||||
|
||||
module = runpy.run_path(str(HELPER))
|
||||
check_server = module["check_server"]
|
||||
|
||||
|
||||
class TestSrsDnsSpf(unittest.TestCase):
|
||||
|
||||
DOMAIN = "srs.example.org"
|
||||
HOSTNAME = "mail.example.org"
|
||||
IPV4 = "192.0.2.25"
|
||||
IPV6 = "2001:db8::25"
|
||||
|
||||
def check_with_spf(self, txt_records):
|
||||
answers = {
|
||||
(self.DOMAIN, "TXT"): txt_records,
|
||||
(self.DOMAIN, "MX"): [
|
||||
f"10 {self.HOSTNAME}."
|
||||
],
|
||||
(self.HOSTNAME, "A"): [
|
||||
self.IPV4
|
||||
],
|
||||
(self.HOSTNAME, "AAAA"): [
|
||||
self.IPV6
|
||||
],
|
||||
}
|
||||
|
||||
def fake_dig(server, name, kind, authoritative=False):
|
||||
self.assertEqual(server, "192.0.2.53")
|
||||
self.assertTrue(
|
||||
authoritative,
|
||||
"DNS checks must query authoritative nameservers"
|
||||
)
|
||||
return answers.get((name, kind), [])
|
||||
|
||||
output = io.StringIO()
|
||||
|
||||
with patch.dict(
|
||||
check_server.__globals__,
|
||||
{"dig": fake_dig}
|
||||
):
|
||||
with contextlib.redirect_stdout(output):
|
||||
result = check_server(
|
||||
"ns.example.org",
|
||||
"192.0.2.53",
|
||||
self.DOMAIN,
|
||||
self.HOSTNAME,
|
||||
self.IPV4,
|
||||
self.IPV6,
|
||||
)
|
||||
|
||||
return result, output.getvalue()
|
||||
|
||||
def test_valid_spf(self):
|
||||
result, output = self.check_with_spf(
|
||||
['"v=spf1 mx -all"']
|
||||
)
|
||||
|
||||
self.assertEqual(result, (True, True))
|
||||
self.assertIn("[ ok ]", output)
|
||||
|
||||
def test_missing_spf(self):
|
||||
result, output = self.check_with_spf([])
|
||||
|
||||
self.assertEqual(result, (False, False))
|
||||
self.assertIn("SPF is not the approved", output)
|
||||
self.assertIn("[ failed ]", output)
|
||||
|
||||
def test_invalid_spf(self):
|
||||
result, output = self.check_with_spf(
|
||||
['"v=spf1 -all"']
|
||||
)
|
||||
|
||||
self.assertEqual(result, (False, False))
|
||||
self.assertIn("SPF is not the approved", output)
|
||||
self.assertIn("[ failed ]", output)
|
||||
|
||||
def test_multiple_spf_records(self):
|
||||
result, output = self.check_with_spf([
|
||||
'"v=spf1 mx -all"',
|
||||
'"v=spf1 ip4:192.0.2.1 -all"',
|
||||
])
|
||||
|
||||
self.assertEqual(result, (False, False))
|
||||
self.assertIn("SPF is not the approved", output)
|
||||
self.assertIn("[ failed ]", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user