f1289858fd
Include the missing resource id in not-found messages, expire auth to Guest after 401, and document Ingress annotations so nginx does not rewrite 404/405 bodies into branded HTML.
181 lines
6.1 KiB
Python
181 lines
6.1 KiB
Python
"""Tests for cluster, storage, pool and ceph handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import Request
|
|
|
|
from app.api.errors import ApiError
|
|
from app.api.registry import HandlerRegistry
|
|
from app.handlers.ceph import register_ceph_handlers
|
|
from app.handlers.cluster import register_cluster_handlers
|
|
from app.handlers.pools import register_pool_handlers
|
|
from app.handlers.storage import register_storage_handlers
|
|
|
|
|
|
class HandlerPool:
|
|
def __init__(self) -> None:
|
|
self.node_exists = True
|
|
|
|
async def fetch(self, sql: str, *args: object) -> list[dict[str, object]]:
|
|
del args
|
|
if "FROM nodes" in sql and "ORDER BY name" in sql:
|
|
return [{"id": uuid.uuid4(), "name": "pve01", "status": "online"}]
|
|
if "FROM storages" in sql and "DISTINCT storage_id" in sql:
|
|
return [{"storage_id": "local-lvm-pve01"}]
|
|
if "FROM storages s" in sql:
|
|
return [
|
|
{
|
|
"storage_id": "local-lvm-pve01",
|
|
"storage_type": "lvmthin",
|
|
"shared": False,
|
|
"capacity_bytes": 1_000_000,
|
|
"used_bytes": 250_000,
|
|
"config": '{"content":["images"]}',
|
|
}
|
|
]
|
|
if "ceph-osd" in sql:
|
|
return [
|
|
{
|
|
"external_id": "osd.0",
|
|
"state": '{"osd_id":0,"status":"up","in":true,"weight":1.0}',
|
|
}
|
|
]
|
|
if "FROM pools" in sql:
|
|
return [
|
|
{
|
|
"id": uuid.uuid4(),
|
|
"pool_id": "production",
|
|
"comment": "prod",
|
|
"metadata": '{"members":["100"]}',
|
|
}
|
|
]
|
|
if "FROM pool_members" in sql:
|
|
return [{"external_id": "100"}]
|
|
if "FROM task_logs" in sql:
|
|
return [{"message": "seeded task", "sequence": 1}]
|
|
if "FROM tasks" in sql:
|
|
return [{"upid": "UPID:pve01:1:1:1:qmstart:100:root@pam:"}]
|
|
if "FROM storage_contents" in sql or "FROM backups" in sql:
|
|
return []
|
|
raise AssertionError(sql)
|
|
|
|
async def fetchrow(self, sql: str, *args: object) -> dict[str, object] | None:
|
|
del args
|
|
if "FROM nodes WHERE name" in sql:
|
|
return {"name": "pve01", "status": "online"} if self.node_exists else None
|
|
if "FROM clusters" in sql:
|
|
return {"metadata": '{"options":{"keyboard":"de-ch"}}'}
|
|
if "FROM storages" in sql:
|
|
return {
|
|
"storage_id": "local-lvm-pve01",
|
|
"storage_type": "lvmthin",
|
|
"shared": False,
|
|
"capacity_bytes": 1_000_000,
|
|
"used_bytes": 250_000,
|
|
"config": '{"content":["images"]}',
|
|
"node_name": "pve01",
|
|
"resource_id": uuid.uuid4(),
|
|
}
|
|
if "ceph-osd" in sql:
|
|
return {
|
|
"external_id": "osd.0",
|
|
"state": '{"osd_id":0,"status":"up","in":true,"weight":1.0,"size_bytes":1000}',
|
|
}
|
|
if "storage_type='ceph'" in sql:
|
|
return {"capacity_bytes": 5_000_000, "used_bytes": 3_000_000}
|
|
raise AssertionError(sql)
|
|
|
|
async def fetchval(self, sql: str, *args: object) -> Any:
|
|
del args
|
|
if "EXISTS(SELECT 1 FROM nodes" in sql:
|
|
return self.node_exists
|
|
if "MAX(external_id::integer)" in sql:
|
|
return 150
|
|
if "count(*)::int FROM resources WHERE kind='ceph-osd'" in sql:
|
|
return 300
|
|
if "SELECT resource_id FROM storages" in sql:
|
|
return uuid.uuid4()
|
|
return False
|
|
|
|
async def execute(self, sql: str, *args: object) -> str:
|
|
del sql, args
|
|
return "UPDATE 1"
|
|
|
|
|
|
def _request(pool: HandlerPool) -> Request:
|
|
app = type("App", (), {})()
|
|
app.state = type("State", (), {"database": type("DB", (), {"pool": pool})()})()
|
|
scope = {
|
|
"type": "http",
|
|
"method": "GET",
|
|
"path": "/",
|
|
"headers": [],
|
|
"query_string": b"",
|
|
"client": ("test", 1234),
|
|
"server": ("test", 80),
|
|
"scheme": "http",
|
|
"root_path": "",
|
|
"app": app,
|
|
}
|
|
request = Request(scope)
|
|
request.state.principal = "root@pam"
|
|
return request
|
|
|
|
|
|
async def _call(handler: Any, values: dict[str, Any], pool: HandlerPool | None = None) -> Any:
|
|
return await handler(_request(pool or HandlerPool()), {"values": values})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cluster_status_and_nextid() -> None:
|
|
registry = HandlerRegistry()
|
|
register_cluster_handlers(registry)
|
|
status = await _call(registry.get("/cluster/status", "GET"), {})
|
|
assert status[0]["name"] == "pve01"
|
|
nextid = await _call(registry.get("/cluster/nextid", "GET"), {})
|
|
assert nextid == 151
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_and_ceph_handlers() -> None:
|
|
registry = HandlerRegistry()
|
|
register_storage_handlers(registry)
|
|
register_ceph_handlers(registry)
|
|
storage = await _call(
|
|
registry.get("/nodes/{node}/storage", "GET"),
|
|
{"node": "pve01"},
|
|
)
|
|
assert storage[0]["storage"] == "local-lvm-pve01"
|
|
osds = await _call(
|
|
registry.get("/nodes/{node}/ceph/osd", "GET"),
|
|
{"node": "pve01"},
|
|
)
|
|
assert osds[0]["status"] == "up"
|
|
ceph_status = await _call(registry.get("/cluster/ceph/status", "GET"), {})
|
|
assert ceph_status["osdmap"]["num_osds"] == 300
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pools_list() -> None:
|
|
registry = HandlerRegistry()
|
|
register_pool_handlers(registry)
|
|
pools = await _call(registry.get("/pools", "GET"), {})
|
|
assert pools[0]["poolid"] == "production"
|
|
assert pools[0]["members"] == ["100"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_node_returns_404() -> None:
|
|
registry = HandlerRegistry()
|
|
register_storage_handlers(registry)
|
|
pool = HandlerPool()
|
|
pool.node_exists = False
|
|
handler = registry.get("/nodes/{node}/storage", "GET")
|
|
assert handler is not None
|
|
with pytest.raises(ApiError, match=r"No such node \('missing'\)"):
|
|
await handler(_request(pool), {"values": {"node": "missing"}})
|