Fix admin API gaps for users, exchange checks, and parser logs

This commit is contained in:
2026-03-19 16:48:38 +01:00
parent 25176f31b4
commit 941c268d32
22 changed files with 817 additions and 28 deletions

View File

@@ -4,6 +4,7 @@ from contextlib import suppress
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from apps.exchange.models import ExchangeConnection
from apps.exchange.services import ExchangeConnectionService, ExchangeServiceError
from apps.parsers.models import (
ManufacturerRecord,
@@ -75,6 +76,35 @@ class ExchangeConnectionServiceUnitTest(TestCase):
schema_name="public",
)
def test_test_connection_payload_does_not_persist_connection(self):
with patch.object(
ExchangeConnectionService,
"test_connection",
return_value="target_alias",
) as test_connection_mock, patch.object(
ExchangeConnectionService,
"validate_target_structure",
) as validate_mock, patch(
"apps.exchange.services.connections"
) as connections_mock:
connections_mock.databases = {"target_alias": {}}
result = ExchangeConnectionService.test_connection_payload(
server="127.0.0.1",
port=5432,
username="postgres",
password=_db_secret(),
database_name="target_db",
schema_name="public",
)
self.assertEqual(result["status"], "success")
self.assertEqual(
result["message"], "Подключение и структура целевой БД валидны."
)
self.assertEqual(ExchangeConnection.objects.count(), 0)
test_connection_mock.assert_called_once()
validate_mock.assert_called_once()
def test_get_active_connection_raises_when_missing(self):
with self.assertRaisesMessage(
ExchangeServiceError,
@@ -124,6 +154,28 @@ class ExchangeConnectionServiceUnitTest(TestCase):
self.assertEqual(connection.last_error, "boom")
self.assertIsNotNone(connection.last_checked_at)
def test_test_connection_failure_for_unsaved_connection_does_not_try_to_save(self):
connection = ExchangeConnectionFactory.build(last_error="")
db_connection = MagicMock()
db_connection.ensure_connection.side_effect = RuntimeError("boom")
connections_mock = MagicMock()
connections_mock.__getitem__.return_value = db_connection
with patch.object(
ExchangeConnectionService,
"_configure_alias",
return_value="exchange_target_unsaved",
), patch(
"apps.exchange.services.connections", connections_mock
), self.assertRaisesMessage(
ExchangeServiceError,
"Ошибка подключения к целевой БД: boom",
):
ExchangeConnectionService.test_connection(connection)
self.assertEqual(connection.last_error, "boom")
self.assertIsNotNone(connection.last_checked_at)
def test_validate_target_structure_calls_all_validation_steps(self):
connection = ExchangeConnectionFactory()
db_connection = MagicMock()

View File

@@ -18,6 +18,7 @@ class ExchangeViewsTest(APITestCase):
self.user = UserFactory.create_user()
self.admin = UserFactory.create_superuser()
self.connections_url = reverse("api_v1:exchange:connections")
self.test_connection_url = reverse("api_v1:exchange:connections-test")
self.copy_url = reverse("api_v1:exchange:copy")
def test_connections_endpoint_admin_only(self):
@@ -65,6 +66,47 @@ class ExchangeViewsTest(APITestCase):
connection_mock.assert_called_once()
validate_mock.assert_called_once()
@patch("apps.exchange.services.ExchangeConnectionService.test_connection_payload")
def test_test_connection_success(self, test_connection_mock):
payload = {
"server": "127.0.0.1",
"port": 5432,
"username": "postgres",
"password": "secret",
"database_name": "target_db",
"schema_name": "public",
}
test_connection_mock.return_value = {
"status": "success",
"message": "ok",
}
self.client.force_authenticate(self.admin)
response = self.client.post(self.test_connection_url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["data"]["status"], "success")
self.assertEqual(ExchangeConnection.objects.count(), 0)
test_connection_mock.assert_called_once_with(**payload)
@patch("apps.exchange.services.ExchangeConnectionService.test_connection_payload")
def test_test_connection_failure_returns_400(self, test_connection_mock):
payload = {
"server": "127.0.0.1",
"port": 5432,
"username": "postgres",
"password": "secret",
"database_name": "target_db",
"schema_name": "public",
}
test_connection_mock.side_effect = ExchangeServiceError("Connection refused")
self.client.force_authenticate(self.admin)
response = self.client.post(self.test_connection_url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(ExchangeConnection.objects.count(), 0)
@patch("apps.exchange.services.ExchangeConnectionService.test_connection")
def test_create_connection_fail_rolls_back_active(self, connection_mock):
connection_mock.side_effect = ExchangeServiceError("Connection refused")

View File

@@ -179,6 +179,55 @@ class ParsersViewSetTest(APITestCase):
)
self.assertEqual(proxy_detail.status_code, status.HTTP_200_OK)
def test_system_logs_support_search_and_organizations_count(self):
first_log = ParserLoadLogFactory(
source="manufactures",
batch_id=101,
status="success",
error_message="ok",
)
ParserLoadLogFactory(
source="inspections",
batch_id=202,
status="failed",
error_message="timeout",
)
ManufacturerRecordFactory(load_batch=101, inn="7701000001")
ManufacturerRecordFactory(load_batch=101, inn="7701000002")
self.client.force_authenticate(self.admin)
response = self.client.get(
reverse("api_v1:system:parser-logs-list"),
{"search": "101"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
rows = response.data["data"]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["id"], first_log.id)
self.assertEqual(rows[0]["organizations_count"], 2)
def test_system_logs_export_returns_csv(self):
ParserLoadLogFactory(
source="manufactures",
batch_id=333,
status="success",
records_count=4,
)
ManufacturerRecordFactory(load_batch=333, inn="7701000001")
self.client.force_authenticate(self.admin)
response = self.client.get(
reverse("api_v1:system:parser-logs-export"),
{"search": "333"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8")
content = response.content.decode("utf-8")
self.assertIn("organizations_count", content)
self.assertIn("333", content)
def test_fns_upload_invalid_filename(self):
self.client.force_authenticate(self.admin)
with tempfile.TemporaryDirectory() as tmpdir:

View File

@@ -62,6 +62,7 @@ class ProfileFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
first_name = factory.LazyAttribute(lambda _: fake.first_name())
middle_name = factory.LazyAttribute(lambda _: fake.first_name())
last_name = factory.LazyAttribute(lambda _: fake.last_name())
bio = factory.LazyAttribute(lambda _: fake.text(max_nb_chars=200))
date_of_birth = factory.LazyAttribute(
@@ -81,6 +82,8 @@ class ProfileFactory(factory.django.DjangoModelFactory):
# Заполняем поля faker'ом, если не переданы
if "first_name" not in kwargs:
profile.first_name = fake.first_name()
if "middle_name" not in kwargs:
profile.middle_name = fake.first_name()
if "last_name" not in kwargs:
profile.last_name = fake.last_name()
if "bio" not in kwargs:

View File

@@ -65,8 +65,10 @@ class ProfileModelTest(TestCase):
self.assertEqual(self.profile.user, self.user)
# Проверяем, что имена заполнены faker'ом
self.assertIsNotNone(self.profile.first_name)
self.assertIsNotNone(self.profile.middle_name)
self.assertIsNotNone(self.profile.last_name)
self.assertTrue(len(self.profile.first_name) > 0)
self.assertTrue(len(self.profile.middle_name) > 0)
self.assertTrue(len(self.profile.last_name) > 0)
def test_profile_str_representation(self):
@@ -90,6 +92,12 @@ class ProfileModelTest(TestCase):
self.assertTrue(field.blank)
self.assertTrue(field.null)
def test_profile_middle_name_optional(self):
"""Test middle_name field is optional"""
field = self.profile._meta.get_field("middle_name")
self.assertTrue(field.blank)
self.assertTrue(field.null)
def test_profile_bio_optional(self):
"""Test bio field is optional"""
field = self.profile._meta.get_field("bio")
@@ -112,17 +120,27 @@ class ProfileModelTest(TestCase):
"""Test full_name property"""
# Test with both names
first_name = fake.first_name()
middle_name = fake.first_name()
last_name = fake.last_name()
self.profile.first_name = first_name
self.profile.middle_name = middle_name
self.profile.last_name = last_name
self.assertEqual(self.profile.full_name, f"{first_name} {last_name}")
self.assertEqual(
self.profile.full_name, f"{first_name} {middle_name} {last_name}"
)
# Test with only first name
self.profile.middle_name = ""
self.profile.last_name = ""
self.assertEqual(self.profile.full_name, first_name)
# Test with first and middle name
self.profile.middle_name = middle_name
self.assertEqual(self.profile.full_name, f"{first_name} {middle_name}")
# Test with only last name
self.profile.first_name = ""
self.profile.middle_name = ""
self.profile.last_name = last_name
self.assertEqual(self.profile.full_name, last_name)

View File

@@ -181,11 +181,27 @@ class AdminUserCreateSerializerTest(TestCase):
"is_active": True,
"is_verified": False,
"first_name": fake.first_name(),
"middle_name": fake.first_name(),
"last_name": fake.last_name(),
}
)
self.assertTrue(serializer.is_valid(), serializer.errors)
def test_admin_user_create_requires_first_and_last_name(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",
}
)
self.assertFalse(serializer.is_valid())
self.assertIn("first_name", serializer.errors)
self.assertIn("last_name", serializer.errors)
class AdminUserUpdateSerializerTest(TestCase):
"""Tests for AdminUserUpdateSerializer."""
@@ -200,6 +216,7 @@ class AdminUserUpdateSerializerTest(TestCase):
"role": "admin",
"is_active": False,
"first_name": fake.first_name(),
"middle_name": fake.first_name(),
},
partial=True,
)
@@ -217,6 +234,7 @@ class ProfileUpdateSerializerTest(TestCase):
"""Test valid profile update data"""
update_data = {
"first_name": fake.first_name(),
"middle_name": fake.first_name(),
"last_name": fake.last_name(),
"bio": fake.text(max_nb_chars=200),
"date_of_birth": str(fake.date_of_birth(minimum_age=18, maximum_age=80)),
@@ -229,13 +247,21 @@ class ProfileUpdateSerializerTest(TestCase):
updated_profile = serializer.save()
self.assertEqual(updated_profile.first_name, update_data["first_name"])
self.assertEqual(updated_profile.middle_name, update_data["middle_name"])
self.assertEqual(updated_profile.last_name, update_data["last_name"])
self.assertEqual(updated_profile.bio, update_data["bio"])
def test_fields_allowed(self):
"""Test only allowed fields can be updated"""
serializer = ProfileUpdateSerializer()
allowed_fields = ["first_name", "last_name", "bio", "avatar", "date_of_birth"]
allowed_fields = [
"first_name",
"middle_name",
"last_name",
"bio",
"avatar",
"date_of_birth",
]
self.assertEqual(set(serializer.Meta.fields), set(allowed_fields))

View File

@@ -122,6 +122,7 @@ class UserServiceTest(TestCase):
self.user.id,
role=UserService.ROLE_ADMIN,
first_name="Иван",
middle_name="Иванович",
last_name="Иванов",
)
@@ -130,6 +131,7 @@ class UserServiceTest(TestCase):
)
self.assertTrue(updated_user.is_staff)
self.assertEqual(updated_user.profile.first_name, "Иван")
self.assertEqual(updated_user.profile.middle_name, "Иванович")
self.assertEqual(updated_user.profile.last_name, "Иванов")
def test_update_managed_user_updates_password(self):
@@ -161,6 +163,37 @@ class UserServiceTest(TestCase):
user = UserService.deactivate_user(self.user.id)
self.assertFalse(user.is_active)
def test_activate_user_success(self):
"""Test activation of user."""
self.user.is_active = False
self.user.save(update_fields=["is_active"])
user = UserService.activate_user(self.user.id)
self.assertTrue(user.is_active)
def test_get_filtered_users_queryset_searches_by_profile_name(self):
ProfileFactory.create_profile(
user=self.user,
first_name="Найден",
middle_name="Тестович",
last_name="Пользователь",
)
queryset = UserService.get_filtered_users_queryset(search="Тестович")
self.assertEqual(list(queryset.values_list("id", flat=True)), [self.user.id])
def test_get_filtered_users_queryset_orders_by_profile_field(self):
first = UserFactory.create_user()
second = UserFactory.create_user()
ProfileFactory.create_profile(user=first, first_name="Борис")
ProfileFactory.create_profile(user=second, first_name="Алексей")
queryset = UserService.get_filtered_users_queryset(ordering="first_name")
ids = list(queryset.values_list("id", flat=True)[:2])
self.assertEqual(ids, [second.id, first.id])
def test_get_user_capabilities_for_admin(self):
"""Test admin capabilities set."""
admin = UserFactory.create_user(is_staff=True)
@@ -236,6 +269,7 @@ class ProfileServiceTest(TestCase):
self.profile = ProfileFactory.create_profile(user=self.user)
self.profile_data = {
"first_name": fake.first_name(),
"middle_name": fake.first_name(),
"last_name": fake.last_name(),
"bio": fake.text(max_nb_chars=200),
"date_of_birth": str(fake.date_of_birth(minimum_age=18, maximum_age=80)),
@@ -270,6 +304,7 @@ class ProfileServiceTest(TestCase):
self.assertIsNotNone(updated_profile)
self.assertEqual(updated_profile.first_name, self.profile_data["first_name"])
self.assertEqual(updated_profile.middle_name, self.profile_data["middle_name"])
self.assertEqual(updated_profile.last_name, self.profile_data["last_name"])
self.assertEqual(updated_profile.bio, self.profile_data["bio"])
@@ -288,6 +323,7 @@ class ProfileServiceTest(TestCase):
self.assertEqual(profile_data["email"], self.user.email)
self.assertEqual(profile_data["username"], self.user.username)
self.assertEqual(profile_data["first_name"], self.profile.first_name)
self.assertEqual(profile_data["middle_name"], self.profile.middle_name)
self.assertEqual(profile_data["last_name"], self.profile.last_name)
self.assertEqual(profile_data["full_name"], self.profile.full_name)
self.assertEqual(profile_data["bio"], self.profile.bio)

View File

@@ -224,6 +224,34 @@ class AdminUserManagementViewTest(APITestCase):
self.assertIn(self.admin.username, usernames)
self.assertIn(self.user.username, usernames)
def test_admin_can_search_users(self):
ProfileFactory.create_profile(
user=self.user,
first_name="Сергей",
middle_name="Петрович",
last_name="Иванов",
)
another = UserFactory.create_user()
ProfileFactory.create_profile(user=another, first_name="Илья")
response = self.client.get(self.list_url, {"search": "Петрович"})
self.assertEqual(response.status_code, status.HTTP_200_OK)
usernames = [item["username"] for item in response.data]
self.assertEqual(usernames, [self.user.username])
def test_admin_can_order_users(self):
first = UserFactory.create_user()
second = UserFactory.create_user()
ProfileFactory.create_profile(user=first, first_name="Борис")
ProfileFactory.create_profile(user=second, first_name="Алексей")
response = self.client.get(self.list_url, {"ordering": "first_name"})
self.assertEqual(response.status_code, status.HTTP_200_OK)
ordered_ids = [item["id"] for item in response.data]
self.assertLess(ordered_ids.index(second.id), ordered_ids.index(first.id))
def test_admin_can_create_user_with_role(self):
password = fake.password(length=12, special_chars=False)
payload = {
@@ -233,6 +261,7 @@ class AdminUserManagementViewTest(APITestCase):
"password": password,
"role": "admin",
"first_name": "Петр",
"middle_name": "Петрович",
"last_name": "Петров",
}
@@ -243,13 +272,19 @@ class AdminUserManagementViewTest(APITestCase):
self.assertTrue(created.is_staff)
self.assertEqual(response.data["role"], "admin")
self.assertEqual(created.profile.first_name, "Петр")
self.assertEqual(created.profile.middle_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},
{
"role": "admin",
"first_name": "Иван",
"middle_name": "Иванович",
"is_verified": True,
},
format="json",
)
@@ -258,6 +293,7 @@ class AdminUserManagementViewTest(APITestCase):
self.assertTrue(self.user.is_staff)
self.assertTrue(self.user.is_verified)
self.assertEqual(self.user.profile.first_name, "Иван")
self.assertEqual(self.user.profile.middle_name, "Иванович")
def test_admin_can_get_user_detail(self):
url = reverse("api_v1:user:admin-user-detail", args=[self.user.id])
@@ -302,6 +338,17 @@ class AdminUserManagementViewTest(APITestCase):
self.user.refresh_from_db()
self.assertFalse(self.user.is_active)
def test_admin_can_activate_user(self):
self.user.is_active = False
self.user.save(update_fields=["is_active"])
url = reverse("api_v1:user:admin-user-activate", 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.assertTrue(self.user.is_active)
def test_admin_cannot_deactivate_self(self):
url = reverse("api_v1:user:admin-user-deactivate", args=[self.admin.id])
@@ -330,6 +377,7 @@ class ProfileDetailViewTest(APITestCase):
self.update_data = {
"first_name": fake.first_name(),
"middle_name": fake.first_name(),
"last_name": fake.last_name(),
"bio": fake.text(max_nb_chars=200),
}
@@ -340,6 +388,7 @@ class ProfileDetailViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["first_name"], self.profile.first_name)
self.assertEqual(response.data["middle_name"], self.profile.middle_name)
def test_update_profile_success(self):
"""Test successful profile update"""
@@ -347,6 +396,7 @@ class ProfileDetailViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["first_name"], self.update_data["first_name"])
self.assertEqual(response.data["middle_name"], self.update_data["middle_name"])
self.assertEqual(response.data["last_name"], self.update_data["last_name"])
# Verify in database