Files
proxmox-api-simulator/tests/unit/test_access_handlers.py
T
2026-07-13 01:35:01 +03:00

116 lines
3.9 KiB
Python

"""API-token lifecycle handler tests without external services."""
from typing import Any, cast
import pytest
from fastapi import FastAPI, Request
from app.api.errors import ApiError
from app.api.registry import HandlerRegistry
from app.db.pool import AsyncpgDatabase
from app.handlers.access import register_access_handlers
class TokenPool:
def __init__(self) -> None:
self.token: dict[str, Any] | None = None
async def fetch(self, _query: str, _userid: str) -> list[dict[str, Any]]:
return [] if self.token is None else [{"token_id": "test", **self.token}]
async def fetchrow(self, query: str, *arguments: object) -> dict[str, Any] | None:
if "INSERT INTO" in query:
self.token = {
"comment": arguments[3],
"privilege_separation": arguments[5],
"expire": arguments[4],
}
return self.token
if "UPDATE api_tokens" in query:
if self.token is None:
return None
self.token["comment"] = arguments[2]
self.token["privilege_separation"] = arguments[4]
return self.token
return self.token
async def fetchval(self, _query: str, _userid: str) -> bool:
return True
async def execute(self, _query: str, _userid: str, _tokenid: str) -> str:
if self.token is None:
return "DELETE 0"
self.token = None
return "DELETE 1"
class FakeDatabase:
def __init__(self, pool: TokenPool) -> None:
self.pool = pool
def request(pool: TokenPool, principal: str = "root@pam") -> Request:
app = FastAPI()
app.state.database = cast(AsyncpgDatabase, FakeDatabase(pool))
result = Request(
{
"type": "http",
"app": app,
"method": "POST",
"path": "/",
"headers": [],
"query_string": b"",
"server": ("test", 80),
"client": ("test", 123),
"scheme": "http",
}
)
result.state.principal = principal
return result
def values(**items: object) -> dict[str, Any]:
return {"values": items}
async def test_token_lifecycle_returns_secret_once_and_persists_metadata() -> None:
registry = HandlerRegistry()
register_access_handlers(registry)
pool = TokenPool()
http_request = request(pool)
create = registry.get("/access/users/{userid}/token/{tokenid}", "POST")
get = registry.get("/access/users/{userid}/token/{tokenid}", "GET")
update = registry.get("/access/users/{userid}/token/{tokenid}", "PUT")
delete = registry.get("/access/users/{userid}/token/{tokenid}", "DELETE")
list_tokens = registry.get("/access/users/{userid}/token", "GET")
assert create and get and update and delete and list_tokens
created = await create(
http_request,
values(userid="root@pam", tokenid="test", comment="first", privsep=True),
)
assert created["full-tokenid"] == "root@pam!test"
assert created["value"]
assert "value" not in await get(http_request, values(userid="root@pam", tokenid="test"))
assert await list_tokens(http_request, values(userid="root@pam"))
updated = await update(
http_request,
values(userid="root@pam", tokenid="test", comment="second", privsep=False),
)
assert updated["comment"] == "second"
await delete(http_request, values(userid="root@pam", tokenid="test"))
with pytest.raises(ApiError) as missing:
await get(http_request, values(userid="root@pam", tokenid="test"))
assert missing.value.status_code == 404
async def test_token_lifecycle_rejects_non_owner() -> None:
registry = HandlerRegistry()
register_access_handlers(registry)
handler = registry.get("/access/users/{userid}/token", "GET")
assert handler
with pytest.raises(ApiError) as denied:
await handler(request(TokenPool(), "auditor@pve"), values(userid="other@pve"))
assert denied.value.status_code == 403