126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
import unittest
|
|
import inspect
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from fastapi import HTTPException
|
|
from pydantic import ValidationError
|
|
|
|
from app.api import rbac
|
|
from app.api import data_platform
|
|
from app.contracts import UserCreate, UserUpdate
|
|
|
|
|
|
class UserContractTests(unittest.TestCase):
|
|
def test_only_two_data_permissions_are_accepted(self) -> None:
|
|
for permission in ("data_manage", "data_view"):
|
|
body = UserCreate(
|
|
username="user@example.com",
|
|
password="Secure-Pass-2026",
|
|
permission=permission,
|
|
)
|
|
self.assertEqual(body.permission, permission)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
UserCreate(
|
|
username="user@example.com",
|
|
password="Secure-Pass-2026",
|
|
permission="collector",
|
|
)
|
|
|
|
def test_data_view_is_read_only_at_the_api_boundary(self) -> None:
|
|
for function_name in ("records", "export_records"):
|
|
parameter = inspect.signature(
|
|
getattr(data_platform, function_name)
|
|
).parameters["_user"]
|
|
self.assertEqual(parameter.annotation, "DataViewerUser")
|
|
|
|
for function_name in (
|
|
"preview_records_import",
|
|
"import_records",
|
|
"add_record",
|
|
"edit_record",
|
|
"remove_record",
|
|
):
|
|
parameter = inspect.signature(
|
|
getattr(data_platform, function_name)
|
|
).parameters["user"]
|
|
self.assertEqual(parameter.annotation, "AdminUser")
|
|
|
|
|
|
class UserApiTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_create_maps_public_permission_to_one_internal_role(self) -> None:
|
|
create_mock = AsyncMock(
|
|
return_value={
|
|
"id": 8,
|
|
"username": "viewer@example.com",
|
|
"full_name": "Viewer",
|
|
"status": "active",
|
|
"roles": ["operator"],
|
|
}
|
|
)
|
|
body = UserCreate(
|
|
username=" viewer@example.com ",
|
|
password="Secure-Pass-2026",
|
|
full_name="Viewer",
|
|
permission="data_view",
|
|
)
|
|
with (
|
|
patch.object(rbac, "create_user", create_mock),
|
|
patch.object(rbac, "hash_password", return_value="hashed"),
|
|
):
|
|
result = await rbac._create_user(
|
|
body,
|
|
{"username": "admin@example.com", "roles": ["admin"]},
|
|
)
|
|
|
|
create_mock.assert_awaited_once()
|
|
data, roles = create_mock.await_args.args
|
|
self.assertEqual(data["username"], "viewer@example.com")
|
|
self.assertEqual(roles, ["operator"])
|
|
self.assertEqual(result["permission"], "data_view")
|
|
self.assertNotIn("roles", result)
|
|
|
|
async def test_list_hides_internal_roles_behind_two_permissions(self) -> None:
|
|
rows = [
|
|
{"id": 1, "username": "manager", "status": "active", "roles": ["admin"]},
|
|
{"id": 2, "username": "legacy", "status": "active", "roles": ["collector"]},
|
|
]
|
|
with patch.object(rbac, "list_users", AsyncMock(return_value=rows)):
|
|
result = await rbac._list_users(
|
|
{"username": "manager", "roles": ["admin"]}
|
|
)
|
|
|
|
self.assertEqual(
|
|
[item["permission"] for item in result],
|
|
["data_manage", "data_view"],
|
|
)
|
|
self.assertTrue(all("roles" not in item for item in result))
|
|
|
|
async def test_current_manager_cannot_disable_or_demote_itself(self) -> None:
|
|
target = {
|
|
"id": 1,
|
|
"username": "manager@example.com",
|
|
"status": "active",
|
|
"roles": ["admin"],
|
|
}
|
|
current = {"username": "manager@example.com", "roles": ["admin"]}
|
|
update_mock = AsyncMock()
|
|
|
|
for body in (
|
|
UserUpdate(status="disabled"),
|
|
UserUpdate(permission="data_view"),
|
|
):
|
|
with (
|
|
patch.object(rbac, "get_user_by_id", AsyncMock(return_value=target)),
|
|
patch.object(rbac, "update_user", update_mock),
|
|
):
|
|
with self.assertRaises(HTTPException) as raised:
|
|
await rbac._update_user(1, body, current)
|
|
self.assertEqual(raised.exception.status_code, 400)
|
|
|
|
update_mock.assert_not_awaited()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|