"""Tests of the admin interface.""" from collections.abc import AsyncIterator from contextlib import asynccontextmanager import allure import pytest import httpx from allure_commons.types import Severity from sshecret.backend import Client from sshecret.crypto import generate_private_key, generate_public_key_string from .types import AdminServer def make_test_key() -> str: """Generate a test key.""" private_key = generate_private_key() return generate_public_key_string(private_key.public_key()) class BaseAdminTests: """Base admin test class.""" @asynccontextmanager async def http_client( self, admin_server: AdminServer, authenticate: bool = True ) -> AsyncIterator[httpx.AsyncClient]: """Run a client towards the admin rest api.""" admin_url, credentials = admin_server username, password = credentials headers: dict[str, str] | None = None if authenticate: async with httpx.AsyncClient(base_url=admin_url) as client: response = await client.post( "api/v1/token", data={"username": username, "password": password} ) assert response.status_code == 200 data = response.json() assert "access_token" in data token = data["access_token"] headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=admin_url, headers=headers) as client: yield client async def create_client( self, admin_server: AdminServer, name: str, public_key: str | None = None, ) -> Client: """Create a client.""" if not public_key: public_key = make_test_key() new_client = { "name": name, "public_key": public_key, "sources": ["192.0.2.0/24"], } async with self.http_client(admin_server, True) as http_client: response = await http_client.post("api/v1/clients/", json=new_client) assert response.status_code == 200 data = response.json() client = Client.model_validate(data) return client @allure.title("Admin API") class TestAdminAPI(BaseAdminTests): """Tests of the Admin REST API.""" @allure.title("Test health test endpoint") @allure.severity(Severity.TRIVIAL) @pytest.mark.asyncio async def test_health_check( self, admin_server: tuple[str, tuple[str, str]] ) -> None: """Test admin login.""" async with self.http_client(admin_server, False) as client: resp = await client.get("/health") assert resp.status_code == 200 @allure.title("Test login over API") @allure.severity(Severity.BLOCKER) @pytest.mark.asyncio async def test_admin_login(self, admin_server: AdminServer) -> None: """Test admin login.""" async with self.http_client(admin_server, False) as client: resp = await client.get("api/v1/clients/") assert resp.status_code == 401 async with self.http_client(admin_server, True) as client: resp = await client.get("api/v1/clients/") assert resp.status_code == 200 @allure.title("Admin API Client API") class TestAdminApiClients(BaseAdminTests): """Test client routes.""" @allure.title("Test creating a client") @allure.description("Ensure we can create a new client.") @pytest.mark.asyncio async def test_create_client(self, admin_server: AdminServer) -> None: """Test create_client.""" client = await self.create_client(admin_server, "testclient") assert client.id is not None assert client.name == "testclient" @allure.title("Test reading clients") @allure.description("Ensure we can retrieve a list of current clients.") @pytest.mark.asyncio async def test_get_clients(self, admin_server: AdminServer) -> None: """Test get_clients.""" client_names = ["test-db", "test-app", "test-www"] for name in client_names: await self.create_client(admin_server, name) async with self.http_client(admin_server) as http_client: resp = await http_client.get("api/v1/clients/") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) == 3 for entry in data: assert isinstance(entry, dict) client_name = entry.get("name") assert client_name in client_names @allure.title("Test client deletion") @allure.description("Ensure we can delete a client.") @pytest.mark.asyncio async def test_delete_client(self, admin_server: AdminServer) -> None: """Test delete_client.""" await self.create_client(admin_server, name="testclient") async with self.http_client(admin_server) as http_client: resp = await http_client.get("api/v1/clients/") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) == 1 assert data[0]["name"] == "testclient" resp = await http_client.delete("/api/v1/clients/testclient") assert resp.status_code == 200 resp = await http_client.get("api/v1/clients/") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) == 0 @allure.title("Test secret management") class TestAdminApiSecrets(BaseAdminTests): """Test secret management.""" @allure.title("Test adding a secret") @allure.description("Ensure that we can add a secret to a client.") @pytest.mark.asyncio async def test_add_secret(self, admin_server: AdminServer) -> None: """Test add_secret.""" await self.create_client(admin_server, name="testclient") async with self.http_client(admin_server) as http_client: data = { "name": "testsecret", "clients": ["testclient"], "value": "secretstring", } resp = await http_client.post("api/v1/secrets/", json=data) assert resp.status_code == 200 @allure.title("Test read a secret") @allure.description("Ensure that we can retrieve a secret we have stored.") @pytest.mark.asyncio async def test_get_secret(self, admin_server: AdminServer) -> None: """Test get_secret.""" await self.test_add_secret(admin_server) async with self.http_client(admin_server) as http_client: resp = await http_client.get("api/v1/secrets/testsecret") assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) assert data["name"] == "testsecret" assert data["secret"] == "secretstring" assert "testclient" in data["clients"] @allure.title("Test adding a secret with automatic value") @allure.description("Test that we can add a secret where we let the system come up with the value of a given length.") @pytest.mark.asyncio async def test_add_secret_auto(self, admin_server: AdminServer) -> None: """Test adding a secret with an auto-generated value.""" await self.create_client(admin_server, name="testclient") async with self.http_client(admin_server) as http_client: data = { "name": "testsecret", "clients": ["testclient"], "value": {"auto_generate": True, "length": 17}, } resp = await http_client.post("api/v1/secrets/", json=data) assert resp.status_code == 200 resp = await http_client.get("api/v1/secrets/testsecret") assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) assert data["name"] == "testsecret" assert len(data["secret"]) == 17 assert "testclient" in data["clients"] @allure.title("Test updating a secret") @allure.description("Test that we can update the value of a stored secret.") @pytest.mark.asyncio async def test_update_secret(self, admin_server: AdminServer) -> None: """Test updating secrets.""" await self.test_add_secret_auto(admin_server) async with self.http_client(admin_server) as http_client: resp = await http_client.put( "api/v1/secrets/testsecret", json={"value": "secret"}, ) assert resp.status_code == 200 resp = await http_client.get("api/v1/secrets/testsecret") assert resp.status_code == 200 data = resp.json() assert data["secret"] == "secret" resp = await http_client.put( "api/v1/secrets/testsecret", json={"value": {"auto_generate": True, "length": 16}}, ) assert resp.status_code == 200 resp = await http_client.get("api/v1/secrets/testsecret") assert resp.status_code == 200 data = resp.json() assert len(data["secret"]) == 16