"""Verify the HTTPS-verify + automatic HTTP fallback in https_manager._apply. This is the safety net the user asked for: if HTTPS is enabled but does not actually work, the server must not be left unreachable — it falls back to HTTP. The TLS probe is monkey-patched so no live server is required. Run: PYTHONPATH=$(pwd) ./.venv/bin/python docs/tools/test_https_fallback.py """ import importlib.util import os import shutil import tempfile TMPDIR = tempfile.mkdtemp() os.environ['DATABASE_URL'] = f'sqlite:///{TMPDIR}/fb.db' from app.app import create_app # noqa: E402 from app.extensions import db # noqa: E402 from app.models.https_config import HTTPSConfig # noqa: E402 REPO = '/home/scheianu/digiserver-v2' TARGET = os.path.join(TMPDIR, 'Caddyfile') app = create_app('production') with app.app_context(): db.create_all() def load_manager(verify_result): """Import https_manager with I/O redirected and the probe stubbed.""" spec = importlib.util.spec_from_file_location( 'hm_fb', os.path.join(REPO, 'https_manager.py')) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) mod.CaddyConfigGenerator.write_caddyfile = staticmethod( lambda content, path=TARGET: (open(TARGET, 'w').write(content), True)[1]) mod.CaddyConfigGenerator.reload_caddy = staticmethod(lambda: True) mod.verify_https = lambda *a, **k: verify_result return mod def blocks(): return [ln.strip() for ln in open(TARGET).read().splitlines() if ln.strip().startswith(('http://', 'https://', ':80'))] def config(): with app.app_context(): return HTTPSConfig.get_config() print('=' * 68) print('CASE A — HTTPS verifies OK -> stays enabled') print('=' * 68) open(TARGET, 'w').write(':80 {\n respond "x"\n}\n') hm = load_manager((True, 'HTTP 200 from https://192.168.0.152:443/api/health')) rc = hm._apply(app, https_enabled=True, hostname='digiserver', domain='', email=None, ip_address='192.168.0.152', port=443, http_fallback=True, verify=True) c = config() print('exit code:', rc, '| https_enabled:', c.https_enabled) print('blocks:', blocks()) assert rc == 0 assert c.https_enabled is True assert 'tls internal' in open(TARGET).read() print('PASS: HTTPS left enabled\n') print('=' * 68) print('CASE B — HTTPS probe FAILS -> automatic HTTP fallback') print('=' * 68) open(TARGET, 'w').write(':80 {\n respond "x"\n}\n') hm = load_manager((False, 'ConnectionRefusedError: refused')) rc = hm._apply(app, https_enabled=True, hostname='digiserver', domain='', email=None, ip_address='192.168.0.152', port=443, http_fallback=True, verify=True) c = config() text = open(TARGET).read() print('exit code:', rc, '| https_enabled:', c.https_enabled) print('blocks:', blocks()) assert rc == 1, f'expected exit 1 signalling the fallback, got {rc}' assert c.https_enabled is False, 'must revert to HTTP-only' assert 'tls internal' not in text, 'TLS must be removed after fallback' assert ':80 {' in text, 'HTTP must still be served' print('PASS: reverted to plain HTTP so the site stays reachable\n') print('=' * 68) print('CASE C — verify disabled -> trusts the config, no probe') print('=' * 68) open(TARGET, 'w').write(':80 {\n respond "x"\n}\n') hm = load_manager((False, 'should never be called')) rc = hm._apply(app, https_enabled=True, hostname='digiserver', domain='', email=None, ip_address='192.168.0.152', port=443, http_fallback=True, verify=False) c = config() print('exit code:', rc, '| https_enabled:', c.https_enabled) assert rc == 0 assert c.https_enabled is True, 'no probe => config trusted' assert 'tls internal' in open(TARGET).read() print('PASS: configuration trusted without probing\n') print('=' * 68) print('CASE D — HTTP only (https disabled) is never probed') print('=' * 68) open(TARGET, 'w').write(':80 {\n respond "x"\n}\n') hm = load_manager((False, 'n/a')) rc = hm._apply(app, https_enabled=False, hostname=None, domain=None, email=None, ip_address=None, port=443, http_fallback=True, verify=True) c = config() print('exit code:', rc, '| https_enabled:', c.https_enabled) assert rc == 0 assert c.https_enabled is False assert 'tls internal' not in open(TARGET).read() print('PASS: no TLS emitted, no probe performed\n') shutil.rmtree(TMPDIR, ignore_errors=True) print('ALL FALLBACK CASES PASSED')