feat(backend): added auth, reviews, users modules
also provided tests
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = "api.v1.users"
|
||||
label = "api_v1_users"
|
||||
@@ -0,0 +1,192 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import Router
|
||||
|
||||
from api.v1.users.schemas import (
|
||||
UserCreateIn,
|
||||
UserListOut,
|
||||
UserOut,
|
||||
UserRoleAssignIn,
|
||||
UserUpdateIn,
|
||||
)
|
||||
from apps.users.auth.bearer import jwt_bearer, require_admin
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.selectors import user_list
|
||||
from apps.users.services import (
|
||||
user_assign_role,
|
||||
user_create,
|
||||
user_delete,
|
||||
user_update,
|
||||
)
|
||||
|
||||
router = Router(tags=["users"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response={HTTPStatus.OK: UserListOut},
|
||||
auth=jwt_bearer,
|
||||
summary="List users",
|
||||
description=(
|
||||
"Return a filtered, paginated list of platform users. Admin only."
|
||||
),
|
||||
)
|
||||
@require_admin
|
||||
def list_users(
|
||||
request: HttpRequest,
|
||||
role: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
search: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> tuple[int, UserListOut]:
|
||||
qs = user_list(role=role, is_active=is_active, search=search)
|
||||
total = qs.count()
|
||||
items = [
|
||||
UserOut.model_validate(item) for item in qs[offset : offset + limit]
|
||||
]
|
||||
|
||||
return HTTPStatus.OK, UserListOut(count=total, items=items)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response={201: UserOut},
|
||||
auth=jwt_bearer,
|
||||
summary="Create user",
|
||||
description=(
|
||||
"Create a new platform user with the specified role. Admin only."
|
||||
),
|
||||
)
|
||||
@require_admin
|
||||
def create_user(
|
||||
request: HttpRequest,
|
||||
payload: UserCreateIn,
|
||||
) -> tuple[int, UserOut]:
|
||||
valid_roles = {choice[0] for choice in UserRole.choices}
|
||||
if payload.role not in valid_roles:
|
||||
raise ValidationError(
|
||||
{
|
||||
"role": (
|
||||
f"Invalid role '{payload.role}'. "
|
||||
f"Must be one of: {', '.join(sorted(valid_roles))}"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
user = user_create(
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
password=payload.password,
|
||||
role=payload.role,
|
||||
first_name=payload.first_name,
|
||||
last_name=payload.last_name,
|
||||
)
|
||||
|
||||
return HTTPStatus.CREATED, UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}",
|
||||
response={HTTPStatus.OK: UserOut},
|
||||
auth=jwt_bearer,
|
||||
summary="Get user",
|
||||
description="Retrieve a single user by their UUID. Admin only.",
|
||||
)
|
||||
@require_admin
|
||||
def get_user(
|
||||
request: HttpRequest,
|
||||
user_id: str,
|
||||
) -> tuple[int, UserOut]:
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
return HTTPStatus.OK, UserOut.model_validate(user)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{user_id}",
|
||||
response={HTTPStatus.OK: UserOut},
|
||||
auth=jwt_bearer,
|
||||
summary="Update user",
|
||||
description=(
|
||||
"Partially update an existing user. "
|
||||
"Only non-null fields in the payload are applied. Admin only."
|
||||
),
|
||||
)
|
||||
@require_admin
|
||||
def update_user(
|
||||
request: HttpRequest,
|
||||
user_id: str,
|
||||
payload: UserUpdateIn,
|
||||
) -> tuple[int, UserOut]:
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
if payload.role is not None:
|
||||
valid_roles = {choice[0] for choice in UserRole.choices}
|
||||
if payload.role not in valid_roles:
|
||||
raise ValidationError(
|
||||
{
|
||||
"role": (
|
||||
f"Invalid role '{payload.role}'. "
|
||||
f"Must be one of: {', '.join(sorted(valid_roles))}"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
updated_user = user_update(
|
||||
user=user,
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
password=payload.password,
|
||||
role=payload.role,
|
||||
is_active=payload.is_active,
|
||||
first_name=payload.first_name,
|
||||
last_name=payload.last_name,
|
||||
)
|
||||
return HTTPStatus.OK, UserOut.model_validate(updated_user)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{user_id}",
|
||||
response={204: None},
|
||||
auth=jwt_bearer,
|
||||
summary="Delete user",
|
||||
description="Permanently delete a user. Admin only.",
|
||||
)
|
||||
@require_admin
|
||||
def delete_user(
|
||||
request: HttpRequest,
|
||||
user_id: str,
|
||||
) -> tuple[int, None]:
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
current_user = getattr(request, "auth", None)
|
||||
if not isinstance(current_user, User):
|
||||
raise ValidationError({"user": "Authentication required."})
|
||||
if str(user.pk) == str(current_user.pk):
|
||||
raise ValidationError({"user": "You cannot delete your own account."})
|
||||
|
||||
user_delete(user=user)
|
||||
|
||||
return HTTPStatus.NO_CONTENT, None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{user_id}/role",
|
||||
response={HTTPStatus.OK: UserOut},
|
||||
auth=jwt_bearer,
|
||||
summary="Assign role",
|
||||
description="Change the platform role of an existing user. Admin only.",
|
||||
)
|
||||
@require_admin
|
||||
def assign_role(
|
||||
request: HttpRequest,
|
||||
user_id: str,
|
||||
payload: UserRoleAssignIn,
|
||||
) -> tuple[int, UserOut]:
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
updated_user = user_assign_role(user=user, role=payload.role)
|
||||
return HTTPStatus.OK, UserOut.model_validate(updated_user)
|
||||
@@ -0,0 +1,123 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from ninja import ModelSchema, Schema
|
||||
from pydantic import Field
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class UserOut(ModelSchema):
|
||||
first_name: str = Field("", alias="firstName")
|
||||
last_name: str = Field("", alias="lastName")
|
||||
is_active: bool
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
User.id.field.name,
|
||||
User.username.field.name,
|
||||
User.email.field.name,
|
||||
User.role.field.name,
|
||||
User.first_name.field.name,
|
||||
User.last_name.field.name,
|
||||
User._meta.get_field("is_active").name,
|
||||
)
|
||||
|
||||
|
||||
class UserCreateIn(Schema):
|
||||
username: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=150,
|
||||
description="Unique username for the new account.",
|
||||
)
|
||||
email: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=254,
|
||||
description="Email address.",
|
||||
)
|
||||
password: str = Field(
|
||||
...,
|
||||
min_length=8,
|
||||
max_length=128,
|
||||
description="Account password (min 8 characters).",
|
||||
)
|
||||
role: str = Field(
|
||||
"viewer",
|
||||
description=(
|
||||
"Platform role to assign. "
|
||||
"One of: admin, experimenter, approver, viewer."
|
||||
),
|
||||
)
|
||||
first_name: str = Field(
|
||||
"",
|
||||
alias="firstName",
|
||||
max_length=150,
|
||||
description="First name.",
|
||||
)
|
||||
last_name: str = Field(
|
||||
"",
|
||||
alias="lastName",
|
||||
max_length=150,
|
||||
description="Last name.",
|
||||
)
|
||||
|
||||
|
||||
class UserUpdateIn(Schema):
|
||||
username: str | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
max_length=150,
|
||||
description="New username.",
|
||||
)
|
||||
email: str | None = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
max_length=254,
|
||||
description="New email address.",
|
||||
)
|
||||
password: str | None = Field(
|
||||
None,
|
||||
min_length=8,
|
||||
max_length=128,
|
||||
description="New password (min 8 characters).",
|
||||
)
|
||||
role: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"New platform role. One of: admin, experimenter, approver, viewer."
|
||||
),
|
||||
)
|
||||
first_name: str | None = Field(
|
||||
None,
|
||||
alias="firstName",
|
||||
max_length=150,
|
||||
description="New first name.",
|
||||
)
|
||||
last_name: str | None = Field(
|
||||
None,
|
||||
alias="lastName",
|
||||
max_length=150,
|
||||
description="New last name.",
|
||||
)
|
||||
is_active: bool | None = Field(
|
||||
None,
|
||||
alias="isActive",
|
||||
description="Set active/inactive status.",
|
||||
)
|
||||
|
||||
|
||||
class UserRoleAssignIn(Schema):
|
||||
role: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Platform role to assign. "
|
||||
"One of: admin, experimenter, approver, viewer."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class UserListOut(Schema):
|
||||
count: int
|
||||
items: list[UserOut]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.tests._helpers import _auth_header, _make_user
|
||||
|
||||
|
||||
class BaseUsersAPITest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.client = Client()
|
||||
self.admin: User = _make_user(
|
||||
username="mgmt_admin",
|
||||
email="mgmt_admin@x.com",
|
||||
password="adminpass1",
|
||||
role=UserRole.ADMIN,
|
||||
)
|
||||
self.viewer: User = _make_user(
|
||||
username="mgmt_viewer",
|
||||
email="mgmt_viewer@x.com",
|
||||
password="viewerpass",
|
||||
role=UserRole.VIEWER,
|
||||
)
|
||||
self.admin_auth: str = _auth_header(self.admin)
|
||||
self.viewer_auth: str = _auth_header(self.viewer)
|
||||
@@ -0,0 +1,96 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.tests._helpers import _make_user
|
||||
|
||||
from ._crud_base import BaseUsersAPITest
|
||||
|
||||
|
||||
class UsersAPIDeleteAssignRoleTest(BaseUsersAPITest):
|
||||
def test_delete_user_admin(self) -> None:
|
||||
target: User = _make_user(
|
||||
username="to_delete",
|
||||
email="del@lotty.local",
|
||||
role=UserRole.VIEWER,
|
||||
)
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_user", kwargs={"user_id": str(target.pk)}),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assertFalse(User.objects.filter(pk=target.pk).exists())
|
||||
|
||||
def test_delete_self_denied(self) -> None:
|
||||
resp = self.client.delete(
|
||||
reverse(
|
||||
"api-1:delete_user", kwargs={"user_id": str(self.admin.pk)}
|
||||
),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
|
||||
def test_delete_user_viewer_denied(self) -> None:
|
||||
resp = self.client.delete(
|
||||
reverse(
|
||||
"api-1:delete_user", kwargs={"user_id": str(self.admin.pk)}
|
||||
),
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_delete_user_not_found(self) -> None:
|
||||
resp = self.client.delete(
|
||||
reverse(
|
||||
"api-1:delete_user", kwargs={"user_id": str(uuid.uuid4())}
|
||||
),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_assign_role_admin(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse(
|
||||
"api-1:assign_role", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps({"role": "experimenter"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json()["role"], "experimenter")
|
||||
|
||||
def test_assign_role_invalid(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse(
|
||||
"api-1:assign_role", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps({"role": "megaboss"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
|
||||
def test_assign_role_viewer_denied(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse(
|
||||
"api-1:assign_role", kwargs={"user_id": str(self.admin.pk)}
|
||||
),
|
||||
data=json.dumps({"role": "viewer"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_assign_role_not_found(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse(
|
||||
"api-1:assign_role", kwargs={"user_id": str(uuid.uuid4())}
|
||||
),
|
||||
data=json.dumps({"role": "admin"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -0,0 +1,125 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from ._crud_base import BaseUsersAPITest
|
||||
|
||||
|
||||
class UsersAPIListCreateTest(BaseUsersAPITest):
|
||||
def test_list_users_admin(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = resp.json()
|
||||
self.assertIn("count", data)
|
||||
self.assertIn("items", data)
|
||||
self.assertEqual(data["count"], 2)
|
||||
|
||||
def test_list_users_viewer_denied(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_list_users_unauthenticated(self) -> None:
|
||||
resp = self.client.get(reverse("api-1:list_users"))
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_list_users_filter_by_role(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
data={"role": "admin"},
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["count"], 1)
|
||||
self.assertEqual(data["items"][0]["role"], "admin")
|
||||
|
||||
def test_list_users_search(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
data={"search": "mgmt_viewer"},
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["count"], 1)
|
||||
|
||||
def test_list_users_pagination(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
data={"limit": 1, "offset": 0},
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
data = resp.json()
|
||||
self.assertEqual(len(data["items"]), 1)
|
||||
self.assertEqual(data["count"], 2)
|
||||
|
||||
def test_create_user_admin(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:create_user"),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "new@lotty.local",
|
||||
"password": "newpass123",
|
||||
"role": "experimenter",
|
||||
"firstName": "New",
|
||||
"lastName": "User",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["username"], "newuser")
|
||||
self.assertEqual(data["role"], "experimenter")
|
||||
|
||||
def test_create_user_invalid_role(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:create_user"),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": "bad_role",
|
||||
"email": "bad@lotty.local",
|
||||
"password": "password1",
|
||||
"role": "superadmin",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
|
||||
def test_create_user_viewer_denied(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:create_user"),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": "denied",
|
||||
"email": "denied@lotty.local",
|
||||
"password": "password1",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_create_user_duplicate_username(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:create_user"),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": "mgmt_admin",
|
||||
"email": "other@lotty.local",
|
||||
"password": "password1",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [409, 422, 400, 500])
|
||||
@@ -0,0 +1,93 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from ._crud_base import BaseUsersAPITest
|
||||
|
||||
|
||||
class UsersAPIReadUpdateTest(BaseUsersAPITest):
|
||||
def test_get_user_admin(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:get_user", kwargs={"user_id": str(self.viewer.pk)}),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json()["username"], "mgmt_viewer")
|
||||
|
||||
def test_get_user_not_found(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:get_user", kwargs={"user_id": str(uuid.uuid4())}),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_get_user_viewer_denied(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:get_user", kwargs={"user_id": str(self.admin.pk)}),
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_update_user_admin(self) -> None:
|
||||
resp = self.client.patch(
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps(
|
||||
{"username": "renamed_viewer", "role": "approver"}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["username"], "renamed_viewer")
|
||||
self.assertEqual(data["role"], "approver")
|
||||
|
||||
def test_update_user_partial(self) -> None:
|
||||
original_role = self.viewer.role
|
||||
resp = self.client.patch(
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps({"firstName": "Updated"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["role"], original_role)
|
||||
|
||||
def test_update_user_invalid_role(self) -> None:
|
||||
resp = self.client.patch(
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps({"role": "superadmin"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
|
||||
def test_update_user_viewer_denied(self) -> None:
|
||||
resp = self.client.patch(
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(self.admin.pk)}
|
||||
),
|
||||
data=json.dumps({"firstName": "Hacked"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.viewer_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_update_user_not_found(self) -> None:
|
||||
resp = self.client.patch(
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(uuid.uuid4())}
|
||||
),
|
||||
data=json.dumps({"firstName": "Ghost"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.tests._helpers import _auth_header, _make_user
|
||||
|
||||
|
||||
class RoleBasedAccessControlTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.client = Client()
|
||||
self.roles = {}
|
||||
for role_val in [
|
||||
UserRole.ADMIN,
|
||||
UserRole.EXPERIMENTER,
|
||||
UserRole.APPROVER,
|
||||
UserRole.VIEWER,
|
||||
]:
|
||||
user: User = _make_user(
|
||||
username=f"rbac_{role_val}",
|
||||
email=f"rbac_{role_val}@x.com",
|
||||
password="password1",
|
||||
role=role_val,
|
||||
)
|
||||
self.roles[role_val] = {
|
||||
"user": user,
|
||||
"auth": _auth_header(user),
|
||||
}
|
||||
|
||||
def test_admin_can_list(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.ADMIN]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_experimenter_cannot_list(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.EXPERIMENTER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_approver_cannot_list(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.APPROVER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_viewer_cannot_list(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_users"),
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.VIEWER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_experimenter_cannot_create(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:create_user"),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": "blocked",
|
||||
"email": "blocked@x.com",
|
||||
"password": "password1",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.EXPERIMENTER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_approver_cannot_delete(self) -> None:
|
||||
target = self.roles[UserRole.VIEWER]["user"]
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_user", kwargs={"user_id": str(target.pk)}),
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.APPROVER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_viewer_cannot_assign_role(self) -> None:
|
||||
target = self.roles[UserRole.EXPERIMENTER]["user"]
|
||||
resp = self.client.post(
|
||||
reverse("api-1:assign_role", kwargs={"user_id": str(target.pk)}),
|
||||
data=json.dumps({"role": "admin"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.VIEWER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_experimenter_cannot_update(self) -> None:
|
||||
target = self.roles[UserRole.VIEWER]["user"]
|
||||
resp = self.client.patch(
|
||||
reverse("api-1:update_user", kwargs={"user_id": str(target.pk)}),
|
||||
data=json.dumps({"firstName": "Nope"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.roles[UserRole.EXPERIMENTER]["auth"],
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
Reference in New Issue
Block a user