chore(): test and validation improvements
This commit is contained in:
@@ -37,7 +37,7 @@ router = Router(tags=["users"])
|
||||
@require_admin
|
||||
def list_users(
|
||||
request: HttpRequest,
|
||||
role: str | None = None,
|
||||
role: UserRole | None = None,
|
||||
is_active: bool | None = None, # noqa: FBT001
|
||||
search: str | None = None,
|
||||
limit: int = 50,
|
||||
@@ -54,7 +54,7 @@ def list_users(
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response={201: UserOut},
|
||||
response={HTTPStatus.CREATED: UserOut},
|
||||
auth=jwt_bearer,
|
||||
summary="Create user",
|
||||
description=(
|
||||
@@ -66,25 +66,7 @@ def create_user(
|
||||
request: HttpRequest,
|
||||
payload: UserCreateIn,
|
||||
) -> tuple[HTTPStatus, 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,
|
||||
)
|
||||
user = user_create(**payload.model_dump())
|
||||
|
||||
return HTTPStatus.CREATED, UserOut.model_validate(user)
|
||||
|
||||
@@ -123,34 +105,13 @@ def update_user(
|
||||
) -> tuple[HTTPStatus, 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,
|
||||
)
|
||||
updated_user = user_update(user=user, **payload.model_dump())
|
||||
return HTTPStatus.OK, UserOut.model_validate(updated_user)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{user_id}",
|
||||
response={204: None},
|
||||
response={HTTPStatus.NO_CONTENT: None},
|
||||
auth=jwt_bearer,
|
||||
summary="Delete user",
|
||||
description="Permanently delete a user. Admin only.",
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from ninja import ModelSchema, Schema
|
||||
from pydantic import Field
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.users.models import User, UserRole
|
||||
|
||||
|
||||
class UserOut(ModelSchema):
|
||||
is_active: bool
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
@@ -18,97 +15,41 @@ class UserOut(ModelSchema):
|
||||
User.role.field.name,
|
||||
User.first_name.field.name,
|
||||
User.last_name.field.name,
|
||||
User._meta.get_field("is_active").name,
|
||||
User.is_active.field.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(
|
||||
"",
|
||||
max_length=150,
|
||||
description="First name.",
|
||||
)
|
||||
last_name: str = Field(
|
||||
"",
|
||||
max_length=150,
|
||||
description="Last name.",
|
||||
)
|
||||
class UserCreateIn(ModelSchema):
|
||||
role: UserRole = UserRole.VIEWER
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
User.username.field.name,
|
||||
User.email.field.name,
|
||||
User.password.field.name,
|
||||
User.role.field.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,
|
||||
max_length=150,
|
||||
description="New first name.",
|
||||
)
|
||||
last_name: str | None = Field(
|
||||
None,
|
||||
max_length=150,
|
||||
description="New last name.",
|
||||
)
|
||||
is_active: bool | None = Field(
|
||||
None,
|
||||
description="Set active/inactive status.",
|
||||
)
|
||||
class UserUpdateIn(ModelSchema):
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
User.username.field.name,
|
||||
User.email.field.name,
|
||||
User.first_name.field.name,
|
||||
User.last_name.field.name,
|
||||
)
|
||||
|
||||
|
||||
class UserRoleAssignIn(Schema):
|
||||
role: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Platform role to assign. "
|
||||
"One of: admin, experimenter, approver, viewer."
|
||||
),
|
||||
)
|
||||
role: UserRole
|
||||
|
||||
|
||||
class UserListOut(Schema):
|
||||
|
||||
@@ -30,7 +30,7 @@ class UsersAPIDeleteAssignRoleTest(BaseUsersAPITest):
|
||||
),
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
self.assertEqual(resp.status_code, 422)
|
||||
|
||||
def test_delete_user_viewer_denied(self) -> None:
|
||||
resp = self.client.delete(
|
||||
@@ -71,7 +71,7 @@ class UsersAPIDeleteAssignRoleTest(BaseUsersAPITest):
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
self.assertEqual(resp.status_code, 422)
|
||||
|
||||
def test_assign_role_viewer_denied(self) -> None:
|
||||
resp = self.client.post(
|
||||
|
||||
@@ -92,7 +92,7 @@ class UsersAPIListCreateTest(BaseUsersAPITest):
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [422, 400])
|
||||
self.assertEqual(resp.status_code, 422)
|
||||
|
||||
def test_create_user_viewer_denied(self) -> None:
|
||||
resp = self.client.post(
|
||||
@@ -122,4 +122,4 @@ class UsersAPIListCreateTest(BaseUsersAPITest):
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.admin_auth,
|
||||
)
|
||||
self.assertIn(resp.status_code, [409, 422, 400, 500])
|
||||
self.assertEqual(resp.status_code, 409)
|
||||
|
||||
@@ -34,16 +34,13 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest):
|
||||
reverse(
|
||||
"api-1:update_user", kwargs={"user_id": str(self.viewer.pk)}
|
||||
),
|
||||
data=json.dumps(
|
||||
{"username": "renamed_viewer", "role": "approver"}
|
||||
),
|
||||
data=json.dumps({"username": "renamed_viewer"}),
|
||||
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
|
||||
@@ -59,17 +56,6 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest):
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user