Files
pvemanager/backend/tests/test_auth.py
markmorado 89d70654fc fix(reinstall): use OSTemplate.vmid (not template_vmid) and convert memory bytes->MB
- AttributeError: OSTemplate has no 'template_vmid' field; the actual column is 'vmid'.
- Use OSTemplate.get_vmid_for_node() to support cross-node replicated templates,
  falling back to source_node/vmid otherwise.
- VMInstance.memory is stored in bytes, but Proxmox config expects MB.
2026-04-29 23:48:48 +02:00

114 lines
4.1 KiB
Python

"""Smoke tests for ``app.auth`` — pure functions only (no DB)."""
from __future__ import annotations
import importlib
from datetime import timedelta
import jwt
import pytest
from fastapi import HTTPException
@pytest.fixture(scope="module")
def auth():
return importlib.import_module("app.auth")
@pytest.fixture(scope="module")
def settings():
from app.config import settings as s
return s
class TestPasswordHashing:
@pytest.mark.smoke
def test_hash_then_verify(self, auth):
pw = "Correct-Horse-Battery-Staple-1!"
hashed = auth.get_password_hash(pw)
assert hashed != pw
assert auth.verify_password(pw, hashed) is True
def test_verify_rejects_wrong_password(self, auth):
hashed = auth.get_password_hash("right-password")
assert auth.verify_password("wrong-password", hashed) is False
def test_hash_is_salted(self, auth):
# Two hashes of the same password must differ (random salt).
a = auth.get_password_hash("same")
b = auth.get_password_hash("same")
assert a != b
class TestJWT:
@pytest.mark.smoke
def test_create_then_decode(self, auth):
token = auth.create_access_token({"sub": "alice"})
payload = auth.decode_access_token(token)
assert payload["sub"] == "alice"
assert "exp" in payload and "iat" in payload
def test_session_binding_round_trips(self, auth):
token = auth.create_access_token({"sub": "bob"}, session_token="sess-xyz")
payload = auth.decode_access_token(token)
assert payload["session"] == "sess-xyz"
def test_expired_token_raises_401(self, auth):
token = auth.create_access_token({"sub": "x"}, expires_delta=timedelta(seconds=-1))
with pytest.raises(HTTPException) as exc:
auth.decode_access_token(token)
assert exc.value.status_code == 401
def test_tampered_token_raises_401(self, auth):
token = auth.create_access_token({"sub": "x"})
tampered = token + "tamper"
with pytest.raises(HTTPException) as exc:
auth.decode_access_token(tampered)
assert exc.value.status_code == 401
def test_token_signed_with_wrong_secret_raises_401(self, auth, settings):
bogus = jwt.encode({"sub": "x"}, "different-secret", algorithm=settings.ALGORITHM)
with pytest.raises(HTTPException) as exc:
auth.decode_access_token(bogus)
assert exc.value.status_code == 401
class TestPermissionHelpers:
def test_check_permission_true_for_admin(self, auth, fake_user_factory):
admin = fake_user_factory(is_admin=True)
assert auth.check_permission(admin, "vm:delete") is True
def test_check_permission_false_when_role_lacks_it(self, auth, fake_user_factory):
user = fake_user_factory(permissions={"vm:view": True})
assert auth.check_permission(user, "vm:delete") is False
def test_require_permission_raises_403(self, auth, fake_user_factory):
user = fake_user_factory(permissions={})
with pytest.raises(HTTPException) as exc:
auth.require_permission(user, "vm:delete")
assert exc.value.status_code == 403
class TestClientIPParsing:
def _request_with_headers(self, headers: dict, client_host: str = "10.0.0.1"):
# Minimal stand-in mimicking starlette.Request surface used by get_client_ip.
from types import SimpleNamespace
# headers must be lower-cased keys; .get is what get_client_ip uses.
return SimpleNamespace(
headers={k.lower(): v for k, v in headers.items()},
client=SimpleNamespace(host=client_host),
)
def test_returns_first_valid_xff(self, auth):
req = self._request_with_headers({"X-Forwarded-For": "203.0.113.5, 10.0.0.1"})
assert auth.get_client_ip(req) == "203.0.113.5"
def test_rejects_spoofed_xff(self, auth):
# Non-IP value must be ignored — fall back to client.host.
req = self._request_with_headers(
{"X-Forwarded-For": "not-an-ip; rm -rf /"},
client_host="10.0.0.99",
)
assert auth.get_client_ip(req) == "10.0.0.99"