""" tests/test_security_hardening.py -------------------------------- Covers the security quick-wins: * strong_password() validator — length, letter+digit, common-password blocklist * response security headers — hardened CSP directives + HSTS over HTTPS only """ import pytest from wtforms.validators import ValidationError # ── Password strength validator ───────────────────────────────────────────── class _Field: def __init__(self, data): self.data = data def _accepts(pw, **kw): from app.utils.forms import strong_password try: strong_password(**kw)(None, _Field(pw)) return True except ValidationError: return False def test_strong_password_accepts_reasonable(): assert _accepts('abcd1234') # 8 chars, letter + digit assert _accepts('Tr0ubador!!') # longer, mixed assert _accepts('') # empty is skipped (Optional handles required-ness) def test_strong_password_rejects_weak(): assert not _accepts('short1') # too short (< 8) assert not _accepts('allletters') # no digit assert not _accepts('12345678') # no letter assert not _accepts('password1') # common-password blocklist assert not _accepts('welcome1') # common-password blocklist def test_strong_password_custom_min_length(): assert not _accepts('abcd123', min_length=8) # 7 chars assert _accepts('abcd1234', min_length=8) # ── Response security headers ──────────────────────────────────────────────── @pytest.fixture def client(app): return app.test_client() def test_hardened_csp_present(client): resp = client.get('/auth/login') csp = resp.headers.get('Content-Security-Policy', '') assert "object-src 'none'" in csp assert "base-uri 'self'" in csp assert "form-action 'self'" in csp assert "frame-ancestors 'none'" in csp assert resp.headers.get('X-Content-Type-Options') == 'nosniff' def test_hsts_only_over_https(client): # Plain HTTP request — no HSTS advertised. http = client.get('/auth/login') assert 'Strict-Transport-Security' not in http.headers # Behind a TLS-terminating proxy (X-Forwarded-Proto=https) — HSTS present. https = client.get('/auth/login', headers={'X-Forwarded-Proto': 'https'}) assert https.headers.get('Strict-Transport-Security', '').startswith('max-age=')