bot_host / tests /test_auth.py
ItsBounvy's picture
deploy: english UI + admin creds + dataset wiring
76946d3 verified
Raw History Blame Contribute Delete
6.34 kB
"""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"