File size: 6,339 Bytes
76946d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | """Unit tests for hosting_panel.auth.SecretVault.
Covers the two-tier encryption roundtrip and the legacy single-tier
fallback path -- this is what protects every bot secret at rest.
"""
from __future__ import annotations
import os
os.environ.setdefault("PANEL_SECRET_KEY", "x" * 64)
os.environ.setdefault("PANEL_ENCRYPTION_KEY",
"90mbg1KLmFKBpoSs5nstm7Ew-p3Q-J3ya3Zbuhwbtho=")
import pytest
from hosting_panel.auth import VAULT
# ---- basics ---------------------------------------------------------------
def test_vault_is_configured_when_env_present():
assert VAULT.is_configured is True
def test_generate_user_key_is_urlsafe_base64_44chars():
"""User key is a raw Fernet key, not a Fernet-encrypted token."""
from cryptography.fernet import Fernet
k = VAULT.generate_user_key()
assert isinstance(k, str)
assert len(k) == 44, f"unexpected length: {len(k)}"
# urlsafe-base64 alphabet: A-Z, a-z, 0-9, _, -, =
import re
assert re.fullmatch(r"[A-Za-z0-9_\-=]+", k), f"unexpected chars: {k!r}"
# And the key actually works as a Fernet key (round-trip).
f = Fernet(k.encode())
ct = f.encrypt(b"hi")
assert f.decrypt(ct) == b"hi"
# ---- single-tier (legacy) -------------------------------------------------
def test_single_tier_roundtrip():
ct = VAULT.encrypt("hello")
assert VAULT.decrypt(ct) == "hello"
def test_single_tier_readable_without_user_key():
ct = VAULT.encrypt("hello")
# user_key=None falls back to single-tier decrypt.
assert VAULT.decrypt(ct, user_key=None) == "hello"
def test_empty_plaintext_passes_through():
assert VAULT.encrypt("") == ""
assert VAULT.decrypt("") == ""
def test_empty_ciphertext_returns_empty():
assert VAULT.decrypt(None) == ""
assert VAULT.decrypt("") == ""
# ---- layered --------------------------------------------------------------
def test_layered_roundtrip():
user_key = VAULT.generate_user_key()
ct = VAULT.encrypt("secret-payload", user_key=user_key)
# Two layers -> ciphertext is much longer than plaintext.
assert len(ct) > len("secret-payload") * 2
assert VAULT.decrypt(ct, user_key=user_key) == "secret-payload"
def test_wrong_user_key_falls_back_to_valueerror():
"""Wrong user key on layered data: both paths fail -> ValueError."""
user_key = VAULT.generate_user_key()
ct = VAULT.encrypt("secret", user_key=user_key)
other_key = VAULT.generate_user_key()
with pytest.raises(ValueError):
VAULT.decrypt(ct, user_key=other_key)
def test_no_user_key_on_layered_fails():
"""Layered data without the user key is NOT silently readable."""
user_key = VAULT.generate_user_key()
ct = VAULT.encrypt("secret", user_key=user_key)
# No user_key means only single-tier fallback -> can't decrypt layered.
with pytest.raises(ValueError):
VAULT.decrypt(ct, user_key=None)
def test_unicode_payload_roundtrip():
user_key = VAULT.generate_user_key()
payload = "Привіт, Слава Україні! 🇺🇦 — emoji & unicodé"
ct = VAULT.encrypt(payload, user_key=user_key)
assert VAULT.decrypt(ct, user_key=user_key) == payload
def test_long_payload_roundtrip():
user_key = VAULT.generate_user_key()
payload = "x" * 100_000
ct = VAULT.encrypt(payload, user_key=user_key)
assert VAULT.decrypt(ct, user_key=user_key) == payload
def test_layered_then_legacy_fallback_works_on_single_tier_data():
"""Single-tier ciphertext is readable with ANY user_key (fallback path)."""
user_key = VAULT.generate_user_key()
ct = VAULT.encrypt("plain-secret") # single-tier
assert VAULT.decrypt(ct, user_key=user_key) == "plain-secret"
# ---- password hashing -----------------------------------------------------
def test_password_hash_and_verify():
from hosting_panel.auth import hash_password, verify_password
h = hash_password("correct-horse-battery-staple")
assert h != "correct-horse-battery-staple"
assert verify_password("correct-horse-battery-staple", h) is True
assert verify_password("wrong-password", h) is False
# Same hash verifies twice (no state mutation).
assert verify_password("correct-horse-battery-staple", h) is True
def test_password_hash_uses_bcrypt_format():
from hosting_panel.auth import hash_password
h = hash_password("hunter2")
# bcrypt hashes start with $2a$ / $2b$ / $2y$
assert h.startswith("$2"), f"not a bcrypt hash: {h[:10]}"
# ---- session serializer ---------------------------------------------------
def _make_request(cookies: dict):
"""Build a minimal Starlette Request with given cookies."""
from starlette.requests import Request
scope = {
"type": "http",
"method": "GET",
"path": "/",
"headers": [(b"cookie", f"session={cookies['session']}".encode())]
if "session" in cookies else [],
"query_string": b"",
}
return Request(scope)
def test_session_roundtrip():
from hosting_panel.auth import create_session, parse_session
token = create_session(user_id=42, username="alice", role="admin")
su = parse_session(token)
assert su is not None
assert su.user_id == 42
assert su.username == "alice"
assert su.role == "admin"
def test_session_tampering_fails():
from hosting_panel.auth import create_session, parse_session
token = create_session(user_id=1, username="a", role="admin")
# Flip last two chars.
tampered = token[:-2] + ("AB" if token[-2:] != "AB" else "CD")
assert parse_session(tampered) is None
def test_session_empty_token_returns_none():
"""Garbage / empty input -> None (no crash)."""
from hosting_panel.auth import parse_session
assert parse_session("") is None
assert parse_session("not-a-real-token") is None
assert parse_session("garbage.with.dots") is None
def test_get_session_user_from_cookie():
"""End-to-end: cookie -> SessionUser."""
from starlette.requests import Request
from hosting_panel.auth import create_session, get_session_user
token = create_session(user_id=7, username="bob", role="operator")
request = _make_request({"session": token})
su = get_session_user(request)
assert su is not None
assert su.user_id == 7
assert su.role == "operator" |