Standardize IDs, fix group APIs, fix tests

This commit is contained in:
2025-07-07 16:51:44 +02:00
parent 880d556542
commit 6faed0dbd4
22 changed files with 765 additions and 262 deletions

View File

@ -1,12 +1,16 @@
"""Tests of the admin interface."""
import os
import allure
from dataclasses import dataclass, field
from httpx import Response
import pytest
from allure_commons.types import Severity
from ..types import AdminServer
from .base import BaseAdminTests
from sshecret_admin.services.models import ClientSecretGroup, ClientSecretGroupList
@allure.title("Admin API")
@ -129,7 +133,8 @@ class TestAdminApiSecrets(BaseAdminTests):
assert isinstance(data, dict)
assert data["name"] == "testsecret"
assert data["secret"] == "secretstring"
assert "testclient" in data["clients"]
client_names = [cl["name"] for cl in data["clients"]]
assert "testclient" in client_names
@allure.title("Test adding a secret with automatic value")
@allure.description(
@ -153,7 +158,7 @@ class TestAdminApiSecrets(BaseAdminTests):
assert isinstance(data, dict)
assert data["name"] == "testsecret"
assert len(data["secret"]) == 17
assert "testclient" in data["clients"]
assert "testclient" in [cl["name"] for cl in data["clients"]]
@allure.title("Test updating a secret")
@allure.description("Test that we can update the value of a stored secret.")
@ -182,3 +187,392 @@ class TestAdminApiSecrets(BaseAdminTests):
assert resp.status_code == 200
data = resp.json()
assert len(data["secret"]) == 16
@dataclass(kw_only=True)
class GroupHier:
"""Group hierarchy for testing."""
name: str
secrets: list[str]
path: list[str] = field(default_factory=list)
class TestSecretGroupApi(BaseAdminTests):
"""Test secret group api."""
async def add_group(
self,
admin_server: AdminServer,
group_name: str,
parent: str | None = None,
description: str | None = None,
) -> None:
"""Add a group."""
path = "api/v1/secrets/groups/"
async with self.http_client(admin_server) as http_client:
data = {"name": group_name, "parent_group": parent}
if description:
data["description"] = description
resp = await http_client.post(path, json=data)
assert resp.status_code == 200
async def get_group(self, admin_server: AdminServer, groups: list[str]) -> Response:
"""Get group."""
group_name = "/".join(groups)
path = f"api/v1/secrets/groups/{group_name}/"
async with self.http_client(admin_server) as http_client:
resp = await http_client.get(path)
return resp
async def get_groups(self, admin_server: AdminServer) -> Response:
"""Get groups."""
path = "api/v1/secrets/groups/"
async with self.http_client(admin_server) as http_client:
resp = await http_client.get(path)
return resp
async def add_secret(
self, admin_server: AdminServer, secret_name: str, group: str | None = None
) -> Response:
"""Add a secret."""
async with self.http_client(admin_server) as http_client:
data = {
"name": secret_name,
"value": "secretstring",
}
if group:
data["group"] = group
resp = await http_client.post("api/v1/secrets/", json=data)
return resp
async def add_secret_to_group(
self, admin_server: AdminServer, secret_name: str, groups: list[str] | None
) -> Response:
"""Add a secret to a group.
Secret should be created in advance.
"""
path = f"api/v1/secrets/set-group"
if not groups:
groups = []
groups.insert(0, "")
group_path = "/".join(groups)
async with self.http_client(admin_server) as http_client:
resp = await http_client.post(
path, json={"secret_name": secret_name, "group_path": group_path}
)
return resp
async def delete_secret_group(
self, admin_server: AdminServer, group_path: str
) -> Response:
"""Delete secret group."""
if group_path.startswith("/"):
group_path = group_path[1:]
path = os.path.join(f"/api/v1/secrets/groups", group_path) + "/"
async with self.http_client(admin_server) as http_client:
resp = await http_client.delete(path)
return resp
async def move_secret_group(
self, admin_server: AdminServer, group_path: str, new_path: str
) -> Response:
"""Move a secret group."""
if group_path.startswith("/"):
group_path = group_path[1:]
path = f"/api/v1/secrets/move-group/{group_path}"
async with self.http_client(admin_server) as http_client:
resp = await http_client.post(path, json={"path": new_path})
return resp
async def update_secret_group(
self,
admin_server: AdminServer,
name: str,
group_path: str,
*,
description: str | None = None,
parent: str | None = None,
) -> Response:
"""Update secret group."""
if group_path.startswith("/"):
group_path = group_path[1:]
data = {
"name": name,
"description": description,
"parent_group": parent,
}
path = f"/api/v1/secrets/groups/{group_path}/"
async with self.http_client(admin_server) as http_client:
resp = await http_client.put(path, json=data)
return resp
@pytest.mark.parametrize("group_name", ["test", "test with spaces", "blåbærgrød"])
@pytest.mark.asyncio
async def test_add_group(self, admin_server: AdminServer, group_name: str) -> None:
"""Test adding a group, then getting it."""
await self.add_group(admin_server, group_name)
response = await self.get_group(admin_server, [group_name])
assert response.status_code == 200
# We might as well try to deserialize the group.
group = ClientSecretGroup.model_validate(response.json())
assert group.group_name == group_name
@pytest.mark.parametrize("parent,child", [("parent", "child")])
@pytest.mark.asyncio
async def test_add_nested_group(
self, admin_server: AdminServer, parent: str, child: str
) -> None:
"""Test adding a group with a parent group."""
await self.add_group(admin_server, parent)
await self.add_group(admin_server, child, parent)
response = await self.get_group(admin_server, [parent])
assert response.status_code == 200
parent_group = ClientSecretGroup.model_validate(response.json())
assert parent_group.group_name == parent
assert len(parent_group.children) == 1
assert parent_group.children[0].group_name == child
response = await self.get_group(admin_server, [parent, child])
assert response.status_code == 200
child_group = ClientSecretGroup.model_validate(response.json())
assert child_group.group_name == child
assert child_group.parent_group is not None
assert child_group.parent_group.group_name == parent
assert child_group.path == "/parent/child"
@pytest.mark.parametrize(
"secret_name,group_name,parent_name",
[("test", "group", None), ("test", "child", "parent")],
)
@pytest.mark.asyncio
async def test_add_secret_to_group(
self,
admin_server: AdminServer,
secret_name: str,
group_name: str,
parent_name: str | None,
) -> None:
"""Test adding a secret to a group."""
resp = await self.add_secret(admin_server, secret_name)
assert resp.status_code == 200
groups = [group_name]
if parent_name:
await self.add_group(admin_server, parent_name)
groups = [parent_name, group_name]
await self.add_group(admin_server, group_name, parent_name)
resp = await self.add_secret_to_group(admin_server, secret_name, groups)
assert resp.status_code == 200
resp = await self.get_group(admin_server, groups)
assert resp.status_code == 200
group = ClientSecretGroup.model_validate(resp.json())
assert len(group.entries) == 1
assert group.entries[0].name == secret_name
@pytest.mark.parametrize("groups", [["group1", "group2", "group3"]])
@pytest.mark.asyncio
async def test_get_group_flat(
self, admin_server: AdminServer, groups: list[str]
) -> None:
"""Test getting a list of groups with no recursion."""
for group in groups:
await self.add_group(admin_server, group)
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == len(groups)
@pytest.mark.asyncio
async def test_get_group_tree(self, admin_server: AdminServer) -> None:
"""Test getting a list of groups where recursion exists."""
await self.add_group(admin_server, "root")
await self.add_group(admin_server, "level1", "root")
await self.add_group(admin_server, "level2", "level1")
await self.add_secret(admin_server, "secret1")
await self.add_secret(admin_server, "secret2", "/root/level1")
await self.add_secret(admin_server, "secret3", "/root/level1")
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
# we expect this to be a tree now
assert len(group_list.ungrouped) == 1
assert len(group_list.groups) == 1
assert group_list.groups[0].group_name == "root"
assert len(group_list.groups[0].children) == 1
assert len(group_list.groups[0].children[0].children) == 1
@pytest.mark.asyncio
async def test_move_secret_to_root(self, admin_server: AdminServer) -> None:
"""Test moving a secret to the root."""
await self.add_group(admin_server, "secretgroup")
await self.add_secret(admin_server, "secret1", "/secretgroup")
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.ungrouped) == 0
await self.add_secret_to_group(admin_server, "secret1", None)
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.ungrouped) == 1
@pytest.mark.asyncio
async def test_delete_secret_group(self, admin_server: AdminServer) -> None:
"""Test deleting a secret group."""
await self.add_group(admin_server, "secretgroup")
await self.add_group(admin_server, "othergroup")
await self.add_secret(admin_server, "secret1", "/secretgroup")
await self.add_secret(admin_server, "secret2", "/secretgroup")
await self.add_secret(admin_server, "secret3", "/secretgroup")
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 2
response = await self.delete_secret_group(admin_server, "/secretgroup")
assert response.status_code == 200
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 1
assert len(group_list.ungrouped) == 3
@pytest.mark.asyncio
async def test_nest_group(self, admin_server: AdminServer) -> None:
"""Test moving a group below another group."""
await self.add_group(admin_server, "secretgroup")
await self.add_group(admin_server, "othergroup")
await self.add_group(admin_server, "nested", "/othergroup")
await self.add_secret(admin_server, "testsecret", "/secretgroup")
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 2
response = await self.move_secret_group(
admin_server, "/secretgroup", "/othergroup/nested"
)
assert response.status_code == 200
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 1
assert group_list.groups[0].group_name == "othergroup"
assert len(group_list.groups[0].children) == 1
assert len(group_list.groups[0].children[0].children) == 1
assert group_list.groups[0].children[0].children[0].group_name == "secretgroup"
assert len(group_list.groups[0].children[0].children[0].entries) == 1
@pytest.mark.asyncio
async def test_add_nested_group_by_path(self, admin_server: AdminServer) -> None:
"""Test adding a group directly by path"""
await self.add_group(admin_server, "/secretgroup")
await self.add_group(admin_server, "/secretgroup/othergroup")
await self.add_group(admin_server, "/secretgroup/othergroup/nestedgroup")
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 1
assert len(group_list.groups[0].children) == 1
assert len(group_list.groups[0].children[0].children) == 1
@pytest.mark.asyncio
async def test_unnest_group(self, admin_server: AdminServer) -> None:
"""Test moving a deeply nested group back to the root."""
await self.add_group(admin_server, "/secretgroup")
await self.add_group(admin_server, "/secretgroup/othergroup")
await self.add_group(admin_server, "/secretgroup/othergroup/nestedgroup")
await self.add_secret(
admin_server, "secret1", "/secretgroup/othergroup/nestedgroup"
)
await self.add_secret(
admin_server, "secret2", "/secretgroup/othergroup/nestedgroup"
)
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 1
move_resp = await self.move_secret_group(
admin_server, "/secretgroup/othergroup/nestedgroup", "/"
)
assert move_resp.status_code == 200
response = await self.get_groups(admin_server)
assert response.status_code == 200
group_list = ClientSecretGroupList.model_validate(response.json())
assert len(group_list.groups) == 2
target_group = next(
filter(lambda x: x.group_name == "nestedgroup", group_list.groups)
)
assert len(target_group.entries) == 2
assert target_group.path == "/nestedgroup"
@pytest.mark.parametrize(
"group_name,description,parent",
[
(("test", "test"), ("before", "after"), (None, None)),
(("test", "newname"), ("descr", "descr"), ("parent", "parent")),
(("test", "test"), ("descr", "descr"), ("oldparent", "newparent")),
(("test", "test"), ("descr", "descr"), ("oldparent", None)),
(("oldname", "newname"), ("before", "after"), ("oldparent", "newparent")),
],
)
@pytest.mark.asyncio
async def test_group_update(
self,
admin_server: AdminServer,
group_name: tuple[str, str],
description: tuple[str, str],
parent: tuple[str | None, str | None],
) -> None:
"""Test updating a group"""
name_b, name_a = group_name
descr_b, descr_a = description
parent_b, parent_a = parent
if parent_b:
await self.add_group(admin_server, parent_b, None)
if parent_a and parent_a != parent_b:
await self.add_group(admin_server, parent_a, None)
elif not parent_a:
parent_a = "/"
await self.add_group(admin_server, name_b, parent_b, descr_b)
group_path = name_b
if parent_b:
group_path = f"{parent_b}/{name_b}"
resp = await self.update_secret_group(
admin_server, name_a, group_path, description=descr_a, parent=parent_a
)
assert resp.status_code == 200
group = ClientSecretGroup.model_validate(resp.json())
assert group.group_name == name_a
assert group.description == descr_a
if parent_a and parent_a != "/":
assert group.parent_group is not None
assert group.parent_group.group_name == parent_a
else:
assert group.parent_group is None