feat: expand platform APIs, sources, and test coverage
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
This commit is contained in:
@@ -12,6 +12,10 @@ from django.test import RequestFactory, TestCase
|
||||
from tests.utils.fixtures import fake
|
||||
|
||||
|
||||
def _password() -> str:
|
||||
return fake.password(length=12, special_chars=False)
|
||||
|
||||
|
||||
class UserAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
@@ -23,7 +27,7 @@ class UserAdminTest(TestCase):
|
||||
request.user = User.objects.create_superuser(
|
||||
email=fake.email(),
|
||||
username=fake.user_name(),
|
||||
password="pass",
|
||||
password=_password(),
|
||||
)
|
||||
request.session = {}
|
||||
request._messages = FallbackStorage(request)
|
||||
@@ -33,13 +37,13 @@ class UserAdminTest(TestCase):
|
||||
verified = User.objects.create_user(
|
||||
email=fake.email(),
|
||||
username=fake.user_name(),
|
||||
password="pass",
|
||||
password=_password(),
|
||||
is_verified=True,
|
||||
)
|
||||
unverified = User.objects.create_user(
|
||||
email=fake.email(),
|
||||
username=fake.user_name(),
|
||||
password="pass",
|
||||
password=_password(),
|
||||
is_verified=False,
|
||||
)
|
||||
self.assertIn("span", str(self.admin.is_verified_badge(verified)))
|
||||
@@ -53,7 +57,7 @@ class UserAdminTest(TestCase):
|
||||
def test_actions(self):
|
||||
users = [
|
||||
User.objects.create_user(
|
||||
email=fake.email(), username=fake.user_name(), password="pass"
|
||||
email=fake.email(), username=fake.user_name(), password=_password()
|
||||
)
|
||||
for _ in range(2)
|
||||
]
|
||||
@@ -80,7 +84,7 @@ class ProfileAdminTest(TestCase):
|
||||
|
||||
def test_has_avatar_badge(self):
|
||||
user = User.objects.create_user(
|
||||
email=fake.email(), username=fake.user_name(), password="pass"
|
||||
email=fake.email(), username=fake.user_name(), password=_password()
|
||||
)
|
||||
profile = user.profile
|
||||
self.assertIn("span", str(self.admin.has_avatar(profile)))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Tests for user serializers"""
|
||||
|
||||
from apps.user.serializers import (
|
||||
AdminUserCreateSerializer,
|
||||
AdminUserUpdateSerializer,
|
||||
LoginSerializer,
|
||||
PasswordChangeSerializer,
|
||||
PasswordResetConfirmSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
TokenSerializer,
|
||||
UserRegistrationSerializer,
|
||||
@@ -110,6 +113,9 @@ class UserSerializerTest(TestCase):
|
||||
self.assertEqual(data["email"], self.user.email)
|
||||
self.assertEqual(data["username"], self.user.username)
|
||||
self.assertEqual(data["phone"], self.user.phone)
|
||||
self.assertEqual(data["role"], "user")
|
||||
self.assertEqual(data["role_label"], "Пользователь")
|
||||
self.assertIn("capabilities", data)
|
||||
self.assertEqual(data["is_verified"], self.user.is_verified)
|
||||
self.assertIn("profile", data)
|
||||
self.assertIn("created_at", data)
|
||||
@@ -117,7 +123,16 @@ class UserSerializerTest(TestCase):
|
||||
|
||||
def test_read_only_fields(self):
|
||||
"""Test that read-only fields are not writable"""
|
||||
read_only_fields = ["id", "is_verified", "created_at", "updated_at"]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"is_active",
|
||||
"is_verified",
|
||||
"role",
|
||||
"role_label",
|
||||
"capabilities",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
serializer = UserSerializer()
|
||||
|
||||
for field_name in read_only_fields:
|
||||
@@ -152,6 +167,45 @@ class UserUpdateSerializerTest(TestCase):
|
||||
self.assertEqual(set(serializer.Meta.fields), set(allowed_fields))
|
||||
|
||||
|
||||
class AdminUserCreateSerializerTest(TestCase):
|
||||
"""Tests for AdminUserCreateSerializer."""
|
||||
|
||||
def test_valid_admin_user_create_data(self):
|
||||
serializer = AdminUserCreateSerializer(
|
||||
data={
|
||||
"email": fake.unique.email(),
|
||||
"username": fake.unique.user_name(),
|
||||
"phone": f"+7{fake.numerify('##########')}",
|
||||
"password": fake.password(length=12, special_chars=False),
|
||||
"role": "user",
|
||||
"is_active": True,
|
||||
"is_verified": False,
|
||||
"first_name": fake.first_name(),
|
||||
"last_name": fake.last_name(),
|
||||
}
|
||||
)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
|
||||
class AdminUserUpdateSerializerTest(TestCase):
|
||||
"""Tests for AdminUserUpdateSerializer."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create_user()
|
||||
|
||||
def test_valid_admin_user_update_data(self):
|
||||
serializer = AdminUserUpdateSerializer(
|
||||
self.user,
|
||||
data={
|
||||
"role": "admin",
|
||||
"is_active": False,
|
||||
"first_name": fake.first_name(),
|
||||
},
|
||||
partial=True,
|
||||
)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
|
||||
class ProfileUpdateSerializerTest(TestCase):
|
||||
"""Tests for ProfileUpdateSerializer"""
|
||||
|
||||
@@ -289,3 +343,31 @@ class PasswordChangeSerializerTest(TestCase):
|
||||
serializer = PasswordChangeSerializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("old_password", serializer.errors)
|
||||
|
||||
|
||||
class PasswordResetConfirmSerializerTest(TestCase):
|
||||
"""Tests for PasswordResetConfirmSerializer."""
|
||||
|
||||
def test_valid_payload(self):
|
||||
new_password = fake.password(length=12, special_chars=False)
|
||||
serializer = PasswordResetConfirmSerializer(
|
||||
data={
|
||||
"token": fake.sha1(raw_output=False),
|
||||
"new_password": new_password,
|
||||
"new_password_confirm": new_password,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
def test_passwords_must_match(self):
|
||||
serializer = PasswordResetConfirmSerializer(
|
||||
data={
|
||||
"token": fake.sha1(raw_output=False),
|
||||
"new_password": fake.password(length=12, special_chars=False),
|
||||
"new_password_confirm": fake.password(length=12, special_chars=False),
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("non_field_errors", serializer.errors)
|
||||
|
||||
@@ -33,6 +33,8 @@ class UserServiceTest(TestCase):
|
||||
self.assertEqual(user.username, self.user_data["username"])
|
||||
self.assertTrue(user.check_password(self.user_data["password"]))
|
||||
self.assertFalse(user.is_verified) # Default value
|
||||
self.assertEqual(UserService.get_user_role(user), UserService.ROLE_USER)
|
||||
self.assertTrue(user.groups.filter(name=UserService.ROLE_USER).exists())
|
||||
|
||||
def test_create_user_with_extra_fields(self):
|
||||
"""Test user creation with extra fields"""
|
||||
@@ -47,6 +49,14 @@ class UserServiceTest(TestCase):
|
||||
self.assertEqual(user.phone, extra_data["phone"])
|
||||
self.assertTrue(user.is_verified)
|
||||
|
||||
def test_create_user_with_admin_role(self):
|
||||
"""Test admin role assignment on creation."""
|
||||
user = UserService.create_user(role=UserService.ROLE_ADMIN, **self.user_data)
|
||||
|
||||
self.assertTrue(user.is_staff)
|
||||
self.assertEqual(UserService.get_user_role(user), UserService.ROLE_ADMIN)
|
||||
self.assertTrue(user.groups.filter(name=UserService.ROLE_ADMIN).exists())
|
||||
|
||||
def test_get_user_by_email_found(self):
|
||||
"""Test getting user by existing email"""
|
||||
found_user = UserService.get_user_by_email(self.user.email)
|
||||
@@ -106,6 +116,31 @@ class UserServiceTest(TestCase):
|
||||
with self.assertRaises(NotFoundError):
|
||||
UserService.update_user(nonexistent_id, username=fake.user_name())
|
||||
|
||||
def test_update_managed_user_updates_role_and_profile(self):
|
||||
"""Test admin update flow changes role and profile names."""
|
||||
updated_user = UserService.update_managed_user(
|
||||
self.user.id,
|
||||
role=UserService.ROLE_ADMIN,
|
||||
first_name="Иван",
|
||||
last_name="Иванов",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
UserService.get_user_role(updated_user), UserService.ROLE_ADMIN
|
||||
)
|
||||
self.assertTrue(updated_user.is_staff)
|
||||
self.assertEqual(updated_user.profile.first_name, "Иван")
|
||||
self.assertEqual(updated_user.profile.last_name, "Иванов")
|
||||
|
||||
def test_update_managed_user_updates_password(self):
|
||||
raw_password = fake.password(length=12, special_chars=False)
|
||||
updated_user = UserService.update_managed_user(
|
||||
self.user.id,
|
||||
password=raw_password,
|
||||
)
|
||||
|
||||
self.assertTrue(updated_user.check_password(raw_password))
|
||||
|
||||
def test_delete_user_success(self):
|
||||
"""Test successful user deletion"""
|
||||
user_id = self.user.id
|
||||
@@ -121,6 +156,47 @@ class UserServiceTest(TestCase):
|
||||
with self.assertRaises(NotFoundError):
|
||||
UserService.delete_user(nonexistent_id)
|
||||
|
||||
def test_deactivate_user_success(self):
|
||||
"""Test soft deactivation of user."""
|
||||
user = UserService.deactivate_user(self.user.id)
|
||||
self.assertFalse(user.is_active)
|
||||
|
||||
def test_get_user_capabilities_for_admin(self):
|
||||
"""Test admin capabilities set."""
|
||||
admin = UserFactory.create_user(is_staff=True)
|
||||
capabilities = UserService.get_user_capabilities(admin)
|
||||
|
||||
self.assertTrue(capabilities["can_manage_users"])
|
||||
self.assertTrue(capabilities["can_refresh_dashboard"])
|
||||
self.assertIn("exchange", capabilities["settings_sections"])
|
||||
|
||||
def test_get_user_capabilities_for_regular_user(self):
|
||||
"""Test regular user capabilities set."""
|
||||
capabilities = UserService.get_user_capabilities(self.user)
|
||||
|
||||
self.assertFalse(capabilities["can_manage_users"])
|
||||
self.assertFalse(capabilities["can_refresh_dashboard"])
|
||||
self.assertEqual(capabilities["settings_sections"], [])
|
||||
|
||||
def test_get_user_role_uses_admin_group(self):
|
||||
user = UserFactory.create_user(is_staff=False, is_superuser=False)
|
||||
admin_group = UserService.ensure_role_groups()[UserService.ROLE_ADMIN]
|
||||
user.groups.add(admin_group)
|
||||
|
||||
self.assertEqual(UserService.get_user_role(user), UserService.ROLE_ADMIN)
|
||||
|
||||
def test_assign_role_replaces_previous_role_group(self):
|
||||
user = UserFactory.create_user()
|
||||
|
||||
UserService.assign_role(user, UserService.ROLE_ADMIN)
|
||||
|
||||
self.assertTrue(user.groups.filter(name=UserService.ROLE_ADMIN).exists())
|
||||
self.assertFalse(user.groups.filter(name=UserService.ROLE_USER).exists())
|
||||
|
||||
def test_assign_role_rejects_unknown_role(self):
|
||||
with self.assertRaisesMessage(ValueError, "Unsupported role: root"):
|
||||
UserService.assign_role(self.user, "root")
|
||||
|
||||
def test_get_tokens_for_user(self):
|
||||
"""Test JWT token generation"""
|
||||
tokens = UserService.get_tokens_for_user(self.user)
|
||||
|
||||
@@ -117,6 +117,24 @@ class LoginViewTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_login_invalid_payload_returns_400(self):
|
||||
response = self.client.post(self.login_url, {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("username", response.data)
|
||||
|
||||
|
||||
class LogoutViewTest(APITestCase):
|
||||
def test_logout_returns_success_message(self):
|
||||
user = UserFactory.create_user()
|
||||
tokens = UserService.get_tokens_for_user(user)
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {tokens['access']}")
|
||||
|
||||
response = self.client.post(reverse("api_v1:user:logout"), {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["message"], "Успешный выход")
|
||||
|
||||
|
||||
class CurrentUserViewTest(APITestCase):
|
||||
"""Tests for CurrentUserView"""
|
||||
@@ -135,6 +153,8 @@ class CurrentUserViewTest(APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["id"], self.user.id)
|
||||
self.assertEqual(response.data["email"], self.user.email)
|
||||
self.assertEqual(response.data["role"], "user")
|
||||
self.assertFalse(response.data["capabilities"]["can_refresh_dashboard"])
|
||||
self.assertIn("profile", response.data)
|
||||
|
||||
def test_get_current_user_unauthenticated(self):
|
||||
@@ -178,6 +198,125 @@ class UserUpdateViewTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_update_user_invalid_returns_400(self):
|
||||
response = self.client.patch(self.update_url, {"username": ""}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("username", response.data)
|
||||
|
||||
|
||||
class AdminUserManagementViewTest(APITestCase):
|
||||
"""Tests for admin-only user management endpoints."""
|
||||
|
||||
def setUp(self):
|
||||
self.admin = UserFactory.create_user(is_staff=True)
|
||||
self.user = UserFactory.create_user()
|
||||
self.tokens = UserService.get_tokens_for_user(self.admin)
|
||||
self.user_tokens = UserService.get_tokens_for_user(self.user)
|
||||
self.list_url = reverse("api_v1:user:admin-users")
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.tokens['access']}")
|
||||
|
||||
def test_admin_can_list_users(self):
|
||||
response = self.client.get(self.list_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
usernames = {item["username"] for item in response.data}
|
||||
self.assertIn(self.admin.username, usernames)
|
||||
self.assertIn(self.user.username, usernames)
|
||||
|
||||
def test_admin_can_create_user_with_role(self):
|
||||
password = fake.password(length=12, special_chars=False)
|
||||
payload = {
|
||||
"email": fake.unique.email(),
|
||||
"username": fake.unique.user_name(),
|
||||
"phone": f"+7{fake.numerify('##########')}",
|
||||
"password": password,
|
||||
"role": "admin",
|
||||
"first_name": "Петр",
|
||||
"last_name": "Петров",
|
||||
}
|
||||
|
||||
response = self.client.post(self.list_url, payload, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
created = User.objects.get(username=payload["username"])
|
||||
self.assertTrue(created.is_staff)
|
||||
self.assertEqual(response.data["role"], "admin")
|
||||
self.assertEqual(created.profile.first_name, "Петр")
|
||||
|
||||
def test_admin_can_update_user_and_role(self):
|
||||
url = reverse("api_v1:user:admin-user-detail", args=[self.user.id])
|
||||
|
||||
response = self.client.patch(
|
||||
url,
|
||||
{"role": "admin", "first_name": "Иван", "is_verified": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.user.refresh_from_db()
|
||||
self.assertTrue(self.user.is_staff)
|
||||
self.assertTrue(self.user.is_verified)
|
||||
self.assertEqual(self.user.profile.first_name, "Иван")
|
||||
|
||||
def test_admin_can_get_user_detail(self):
|
||||
url = reverse("api_v1:user:admin-user-detail", args=[self.user.id])
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["id"], self.user.id)
|
||||
|
||||
def test_admin_cannot_patch_self_to_inactive(self):
|
||||
url = reverse("api_v1:user:admin-user-detail", args=[self.admin.id])
|
||||
|
||||
response = self.client.patch(url, {"is_active": False}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data["detail"], "Нельзя деактивировать самого себя.")
|
||||
|
||||
def test_admin_cannot_patch_self_to_regular_user(self):
|
||||
url = reverse("api_v1:user:admin-user-detail", args=[self.admin.id])
|
||||
|
||||
response = self.client.patch(url, {"role": "user"}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(
|
||||
response.data["detail"], "Нельзя снять у себя роль администратора."
|
||||
)
|
||||
|
||||
def test_admin_can_patch_self_with_safe_fields(self):
|
||||
url = reverse("api_v1:user:admin-user-detail", args=[self.admin.id])
|
||||
|
||||
response = self.client.patch(url, {"first_name": "Админ"}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["profile"]["first_name"], "Админ")
|
||||
|
||||
def test_admin_can_deactivate_user(self):
|
||||
url = reverse("api_v1:user:admin-user-deactivate", args=[self.user.id])
|
||||
|
||||
response = self.client.post(url, {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.user.refresh_from_db()
|
||||
self.assertFalse(self.user.is_active)
|
||||
|
||||
def test_admin_cannot_deactivate_self(self):
|
||||
url = reverse("api_v1:user:admin-user-deactivate", args=[self.admin.id])
|
||||
|
||||
response = self.client.post(url, {}, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_regular_user_cannot_access_admin_user_management(self):
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.user_tokens['access']}"
|
||||
)
|
||||
|
||||
response = self.client.get(self.list_url)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
class ProfileDetailViewTest(APITestCase):
|
||||
"""Tests for ProfileDetailView"""
|
||||
@@ -225,6 +364,22 @@ class ProfileDetailViewTest(APITestCase):
|
||||
# Profile should be created automatically
|
||||
self.assertTrue(Profile.objects.filter(user=self.user).exists())
|
||||
|
||||
def test_update_profile_invalid_returns_400(self):
|
||||
response = self.client.patch(
|
||||
self.profile_url,
|
||||
{"date_of_birth": "not-a-date"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("date_of_birth", response.data)
|
||||
|
||||
def test_get_full_profile_endpoint(self):
|
||||
response = self.client.get(reverse("api_v1:user:profile_full"))
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["id"], self.user.id)
|
||||
|
||||
|
||||
class PasswordChangeViewTest(APITestCase):
|
||||
"""Tests for PasswordChangeView"""
|
||||
@@ -316,6 +471,20 @@ class TokenRefreshViewTest(APITestCase):
|
||||
self.assertTrue("errors" in response.data or "detail" in response.data)
|
||||
|
||||
|
||||
class TokenVerifyViewTest(APITestCase):
|
||||
def test_verify_access_token_success(self):
|
||||
user = UserFactory.create_user()
|
||||
tokens = UserService.get_tokens_for_user(user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("api_v1:user:token_verify"),
|
||||
{"token": tokens["access"]},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ApiJwtOnlyAuthenticationTest(APITestCase):
|
||||
"""Tests that API auth flow is JWT-only and not session-cookie based."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user