Sanitize codebase, reorganize docs, and add missing deploy files
Remove dead code identified in docs/SANITIZATION-REVIEW.md:
- app/blueprints/content_old.py and app/blueprints/playlist.py
- app/models/group.py, app/utils/nginx_config_reader.py
- orphaned templates (content_list, edit_content, upload_content,
player_page) and the related group/Template references
Result: 6 blueprints, 82 routes, no dead modules or orphan templates.
Add files that deploy.sh and docker-entrypoint.sh already require but
which were never tracked:
- https_manager.py (referenced by deploy.sh, migrate_network.sh,
docker-entrypoint.sh)
- Caddyfile.example (seeded by deploy.sh; its absence aborts deploy)
Relocate generated Graphify artifacts from graphify-out/ to
docs/graphify-out/ (110 files, no content change) and archive the
superseded docs under docs/.
Ignore hygiene:
- ignore ad-hoc .env backups (.env.bak*) — they contain live secrets
- keep the pre-sanitization snapshots (docs/legacy code/,
docs/old_code_documentation/) on disk but out of the repo
Fix .env.example: drop a duplicated config block, genericize the
hardcoded host IP, and document HOSTNAME_INTERNAL.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Verify the env-driven HTTPS bootstrap in https_manager.py.
|
||||
|
||||
Covers the behaviours the user asked for:
|
||||
1. Both HOSTNAME_INTERNAL + HOST_IP set -> HTTPS configured at startup
|
||||
2. Either missing -> NO-OP, HTTP fallback stays
|
||||
3. Re-running with the same env -> idempotent
|
||||
4. HTTPS_HTTP_FALLBACK=false -> redirect to the published port
|
||||
5. Admin UI change after bootstrap -> overrides the env value
|
||||
|
||||
Run: PYTHONPATH=$(pwd) ./.venv/bin/python docs/tools/test_https_bootstrap.py
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
# DATABASE_URL must be set BEFORE importing the app: ProductionConfig evaluates
|
||||
# it at class-definition (import) time.
|
||||
TMPDIR = tempfile.mkdtemp()
|
||||
DB_PATH = os.path.join(TMPDIR, 'bootstrap.db')
|
||||
os.environ['DATABASE_URL'] = f'sqlite:///{DB_PATH}'
|
||||
|
||||
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')
|
||||
PLACEHOLDER = ':80 {\n respond "placeholder"\n}\n'
|
||||
|
||||
app = create_app('production')
|
||||
|
||||
|
||||
def reset_db():
|
||||
"""Drop and recreate the schema for a clean case."""
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
|
||||
def load_manager():
|
||||
"""Import https_manager with file writes/reloads redirected to TARGET."""
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'hm_boot', 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)
|
||||
return mod
|
||||
|
||||
|
||||
def reset_env():
|
||||
for k in ('HOSTNAME_INTERNAL', 'HOST_IP', 'DOMAIN', 'SSL_EMAIL',
|
||||
'HTTPS_PORT', 'HTTPS_HTTP_FALLBACK', 'HTTPS_VERIFY'):
|
||||
os.environ.pop(k, None)
|
||||
# No live Caddy in these unit tests, so skip the post-reload TLS probe.
|
||||
# The probe + automatic fallback are covered by
|
||||
# docs/tools/test_https_fallback.py.
|
||||
os.environ['HTTPS_VERIFY'] = 'false'
|
||||
|
||||
|
||||
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 1 — both vars set -> HTTPS configured at startup')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152',
|
||||
SSL_EMAIL='admin@example.com')
|
||||
hm = load_manager()
|
||||
rc = hm.bootstrap_from_env(app)
|
||||
c = config()
|
||||
print('exit code:', rc, '| https_enabled:', c.https_enabled, '| domain:', repr(c.domain))
|
||||
print('blocks:', blocks())
|
||||
assert rc == 0
|
||||
assert c.https_enabled is True
|
||||
assert not c.domain, 'empty domain must select the internal CA'
|
||||
assert 'tls internal' in open(TARGET).read()
|
||||
assert 'http://192.168.0.152' in open(TARGET).read()
|
||||
print('PASS: internal CA + HTTP fallback generated\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 2 — HOST_IP missing -> no-op, HTTP fallback remains')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ['HOSTNAME_INTERNAL'] = 'digiserver' # HOST_IP absent
|
||||
hm = load_manager()
|
||||
rc = hm.bootstrap_from_env(app)
|
||||
c = config()
|
||||
print('exit code:', rc, '| config:', 'none' if c is None else c.https_enabled)
|
||||
assert rc == 0, 'must not fail container startup'
|
||||
assert c is None or c.https_enabled is False, 'HTTPS must stay off'
|
||||
assert open(TARGET).read() == PLACEHOLDER, 'Caddyfile must be untouched'
|
||||
print('PASS: stayed on plain-HTTP fallback, Caddyfile untouched\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 3 — HOSTNAME_INTERNAL missing -> no-op')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
os.environ['HOST_IP'] = '192.168.0.152'
|
||||
hm = load_manager()
|
||||
rc = hm.bootstrap_from_env(app)
|
||||
c = config()
|
||||
print('exit code:', rc, '| config:', 'none' if c is None else c.https_enabled)
|
||||
assert c is None or c.https_enabled is False
|
||||
print('PASS: no-op when hostname is missing\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 3b — neither var set (default fresh deploy) -> no-op')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
hm = load_manager()
|
||||
rc = hm.bootstrap_from_env(app)
|
||||
assert config() is None
|
||||
print('exit code:', rc, '| https_config row: none')
|
||||
print('PASS: untouched config, HTTP fallback only\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 4 — idempotency (run twice)')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152')
|
||||
hm = load_manager()
|
||||
hm.bootstrap_from_env(app)
|
||||
first = open(TARGET).read()
|
||||
hm.bootstrap_from_env(app)
|
||||
second = open(TARGET).read()
|
||||
with app.app_context():
|
||||
n = HTTPSConfig.query.count()
|
||||
print('https_config rows:', n, '| Caddyfile identical:', first == second)
|
||||
assert n == 1, f'expected one config row, got {n}'
|
||||
assert first == second, 'Caddyfile changed on re-run'
|
||||
print('PASS: idempotent\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 5 — HTTPS_HTTP_FALLBACK=false -> redirect to the HTTPS URL')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
# HTTPS_PORT stays at its 443 default, so the redirect target must not carry
|
||||
# an explicit port (https://host, not https://host:443).
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152',
|
||||
HTTPS_HTTP_FALLBACK='false')
|
||||
hm = load_manager()
|
||||
hm.bootstrap_from_env(app)
|
||||
text = open(TARGET).read()
|
||||
print('blocks:', blocks())
|
||||
assert 'redir https://192.168.0.152{uri} 301' in text, \
|
||||
'redirect should omit :443 when HTTPS_PORT is 443'
|
||||
print('PASS: redirect targets https://<ip> with no redundant port\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 5b — non-standard HTTPS_PORT -> redirect includes the port')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152',
|
||||
HTTPS_PORT='8443', HTTPS_HTTP_FALLBACK='false')
|
||||
hm = load_manager()
|
||||
hm.bootstrap_from_env(app)
|
||||
text = open(TARGET).read()
|
||||
print('blocks:', blocks())
|
||||
assert 'redir https://192.168.0.152:8443{uri} 301' in text, \
|
||||
'redirect must include a non-standard port'
|
||||
print('PASS: redirect includes :8443\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 6 — admin change BEFORE restart is NOT clobbered by the env')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152')
|
||||
hm = load_manager()
|
||||
hm.bootstrap_from_env(app) # startup bootstrap (env owns it)
|
||||
print('after bootstrap IP :', config().ip_address)
|
||||
|
||||
# Simulate an admin changing the IP in the UI (updated_by = the username).
|
||||
hm._apply(app, https_enabled=True, hostname='digiserver', domain='',
|
||||
email=None, ip_address='10.0.0.99', port=443, http_fallback=True,
|
||||
verify=False)
|
||||
with app.app_context():
|
||||
c = HTTPSConfig.get_config()
|
||||
c.updated_by = 'admin' # as admin.py would record it
|
||||
from app.extensions import db as _db
|
||||
_db.session.commit()
|
||||
print('after admin IP :', config().ip_address)
|
||||
|
||||
# Next container start re-runs the bootstrap with the SAME env.
|
||||
rc = hm.bootstrap_from_env(app)
|
||||
print('bootstrap exit code:', rc)
|
||||
print('IP after restart :', config().ip_address)
|
||||
assert rc == 0
|
||||
assert config().ip_address == '10.0.0.99', \
|
||||
'admin setting must survive an env bootstrap on restart'
|
||||
# The admin-set IP must be served (the hostname from before is still served too,
|
||||
# because the admin only changed the IP field).
|
||||
assert 'https://10.0.0.99 {' in open(TARGET).read(), \
|
||||
'admin IP should be present in the Caddyfile'
|
||||
assert 'https://192.168.0.152 {' not in open(TARGET).read(), \
|
||||
'the old env IP must no longer be served'
|
||||
print('PASS: admin-owned config is preserved across restarts\n')
|
||||
|
||||
print('=' * 68)
|
||||
print('CASE 7 — env still owns config -> env change IS applied on restart')
|
||||
print('=' * 68)
|
||||
reset_db(); reset_env()
|
||||
open(TARGET, 'w').write(PLACEHOLDER)
|
||||
os.environ.update(HOSTNAME_INTERNAL='digiserver', HOST_IP='192.168.0.152')
|
||||
hm = load_manager()
|
||||
hm.bootstrap_from_env(app)
|
||||
print('first start IP :', config().ip_address)
|
||||
os.environ['HOST_IP'] = '10.20.30.40' # operator edits .env
|
||||
hm.bootstrap_from_env(app)
|
||||
print('second start IP:', config().ip_address)
|
||||
assert config().ip_address == '10.20.30.40', 'env change should apply'
|
||||
assert 'https://10.20.30.40' in open(TARGET).read()
|
||||
print('PASS: env changes still take effect while env owns the config\n')
|
||||
|
||||
shutil.rmtree(TMPDIR, ignore_errors=True)
|
||||
print('ALL BOOTSTRAP CASES PASSED')
|
||||
Reference in New Issue
Block a user