116 lines
3.9 KiB
Python
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
|