Добавлены тесты защиты системного клиента и рефакторинг структуры тестов

- Создан новый класс SystemCustomerProtectionTestCase с 5 критичными тестами
- Тест создания системного клиента с правильными атрибутами
- Тест защиты от удаления системного клиента (ValidationError)
- Тест защиты email системного клиента от изменения
- Тест защиты флага is_system_customer от изменения
- Тест что обычные клиенты не затронуты защитой

- Исправлена логика в Customer.save(): проверка теперь использует original.is_system_customer
- Добавлен импорт ValidationError из django.core.exceptions

- Рефакторинг структуры тестов customers:
  - Разделены тесты по отдельным модулям в папке customers/tests/
  - test_search_strategies.py - тесты стратегий поиска
  - test_system_customer.py - тесты защиты системного клиента
  - test_wallet_balance.py - тесты баланса кошелька
  - test_wallet_service.py - тесты WalletService
  - test_wallet_model.py - тесты модели WalletTransaction

- Обновлён анализ тестов: 50 тестов (было 45), все проходят успешно
- Критичная функциональность POS системы теперь покрыта тестами
- Учтена tenant-система (используется TenantTestCase)
This commit is contained in:
2025-12-28 00:32:45 +03:00
parent b1855cc9f0
commit dbbac933af
9 changed files with 754 additions and 507 deletions

View File

@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
"""
Тесты для функций поиска клиентов.
Используем TenantTestCase для корректной работы с tenant-системой.
"""
from django_tenants.test.cases import TenantTestCase
from customers.views import determine_search_strategy, is_query_phone_only
class DetermineSearchStrategyTestCase(TenantTestCase):
"""
Тесты для функции determine_search_strategy().
Компактная версия с параметризацией для избежания дублирования.
"""
def _test_strategy(self, query, expected_strategy, expected_value):
"""Вспомогательный метод для проверки стратегии."""
strategy, search_value = determine_search_strategy(query)
self.assertEqual(strategy, expected_strategy,
f"Query '{query}' должен вернуть стратегию '{expected_strategy}'")
self.assertEqual(search_value, expected_value,
f"Query '{query}' должен вернуть значение '{expected_value}'")
# ===== email_prefix: query заканчивается на @ =====
def test_email_prefix_strategy(self):
"""Различные варианты поиска по префиксу email."""
test_cases = [
('team_x3m@', 'team_x3m'),
('user_name@', 'user_name'),
('test123@', 'test123'),
]
for query, expected_value in test_cases:
self._test_strategy(query, 'email_prefix', expected_value)
# ===== email_domain: query начинается с @ =====
def test_email_domain_strategy(self):
"""Различные варианты поиска по домену email."""
test_cases = [
('@bk', 'bk'),
('@bk.ru', 'bk.ru'),
('@mail.google.com', 'mail.google.com'),
]
for query, expected_value in test_cases:
self._test_strategy(query, 'email_domain', expected_value)
# ===== email_full: query содержит и локальную часть, и домен =====
def test_email_full_strategy(self):
"""Различные варианты полного поиска email."""
test_cases = [
('test@bk.ru', 'test@bk.ru'),
('test@bk', 'test@bk'),
('user.name@mail.example.com', 'user.name@mail.example.com'),
]
for query, expected_value in test_cases:
self._test_strategy(query, 'email_full', expected_value)
# ===== universal: query без @, 3+ символов =====
def test_universal_strategy(self):
"""Универсальный поиск для запросов 3+ символов."""
test_cases = [
('abc', 'abc'), # минимум 3 символа
('natul', 'natul'),
('наталь', 'наталь'), # кириллица
('Test123', 'Test123'), # смешанный
('Ivan Petrov', 'Ivan Petrov'), # с пробелами
]
for query, expected_value in test_cases:
self._test_strategy(query, 'universal', expected_value)
# ===== name_only: очень короткие запросы (< 3 символов без @) =====
def test_name_only_strategy(self):
"""Поиск только по имени для коротких запросов."""
test_cases = [
('t', 't'), # 1 символ
('te', 'te'), # 2 символа
('на', 'на'), # 2 символа кириллица
('', ''), # пустая строка
]
for query, expected_value in test_cases:
self._test_strategy(query, 'name_only', expected_value)
# ===== edge cases =====
def test_edge_cases(self):
"""Граничные и специальные случаи."""
# Только символ @
self._test_strategy('@', 'email_domain', '')
# Множественные @ - берётся первый
self._test_strategy('test@example@com', 'email_full', 'test@example@com')
# ===== real-world критические сценарии =====
def test_real_world_email_prefix_no_false_match(self):
"""
КРИТИЧНЫЙ: query 'team_x3m@' НЕ должен найти 'natulj@bk.ru'.
Проверяем, что используется email_prefix (istartswith), а не universal (icontains).
"""
strategy, search_value = determine_search_strategy('team_x3m@')
self.assertEqual(strategy, 'email_prefix')
self.assertEqual(search_value, 'team_x3m')
# Важно: НЕ universal стратегия
self.assertNotEqual(strategy, 'universal')
def test_real_world_domain_search(self):
"""Real-world: '@bk' находит все email с @bk.*"""
self._test_strategy('@bk', 'email_domain', 'bk')
def test_real_world_universal_search(self):
"""Real-world: 'natul' находит и имя 'Наталья' и email 'natulj@bk.ru'"""
self._test_strategy('natul', 'universal', 'natul')
class IsQueryPhoneOnlyTestCase(TenantTestCase):
"""
Тесты для функции is_query_phone_only().
Проверяют, что функция правильно определяет, содержит ли query
только символы номера телефона (цифры, +, -, (), пробелы).
"""
# ===== Должны вернуть True (только телефонные символы) =====
def test_phone_only_digits(self):
"""Query '295' должен вернуть True (только цифры)"""
self.assertTrue(is_query_phone_only('295'))
def test_phone_only_single_digit(self):
"""Query '5' должен вернуть True (одна цифра)"""
self.assertTrue(is_query_phone_only('5'))
def test_phone_with_plus(self):
"""Query '+375291234567' должен вернуть True"""
self.assertTrue(is_query_phone_only('+375291234567'))
def test_phone_with_dashes(self):
"""Query '029-123-45' должен вернуть True"""
self.assertTrue(is_query_phone_only('029-123-45'))
def test_phone_with_parentheses(self):
"""Query '(029) 123-45' должен вернуть True"""
self.assertTrue(is_query_phone_only('(029) 123-45'))
def test_phone_with_spaces(self):
"""Query '029 123 45' должен вернуть True"""
self.assertTrue(is_query_phone_only('029 123 45'))
def test_phone_complex_format(self):
"""Query '+375 (29) 123-45-67' должен вернуть True"""
self.assertTrue(is_query_phone_only('+375 (29) 123-45-67'))
def test_phone_with_dot(self):
"""Query '029.123.45' должен вернуть True"""
self.assertTrue(is_query_phone_only('029.123.45'))
# ===== Должны вернуть False (содержат буквы или другие символы) =====
def test_query_with_letters_only(self):
"""Query 'abc' должен вернуть False (содержит буквы)"""
self.assertFalse(is_query_phone_only('abc'))
def test_query_with_mixed_letters_digits(self):
"""Query 'x3m' должен вернуть False (содержит буквы)"""
self.assertFalse(is_query_phone_only('x3m'))
def test_query_name_with_digits(self):
"""Query 'team_x3m' должен вернуть False (содержит буквы и _)"""
self.assertFalse(is_query_phone_only('team_x3m'))
def test_query_name_cyrillic(self):
"""Query 'Наталья' должен вернуть False (содержит кириллицу)"""
self.assertFalse(is_query_phone_only('Наталья'))
def test_query_with_underscore(self):
"""Query '123_456' должен вернуть False (содержит _)"""
self.assertFalse(is_query_phone_only('123_456'))
def test_query_with_hash(self):
"""Query '123#456' должен вернуть False (содержит #)"""
self.assertFalse(is_query_phone_only('123#456'))
def test_empty_string(self):
"""Query '' должен вернуть False (пустая строка)"""
self.assertFalse(is_query_phone_only(''))
def test_only_spaces(self):
"""Query ' ' должен вернуть False (пустой запрос)"""
self.assertFalse(is_query_phone_only(' '))
# ===== Real-world cases =====
def test_real_world_case_x3m_should_not_be_phone(self):
"""
Real-world case: "x3m" содержит букву, поэтому НЕ похож на телефон.
Это критично для решения проблемы с поиском Натальи.
"""
self.assertFalse(is_query_phone_only('x3m'))
# Значит, при поиске "x3m" НЕ будет поиска по цифре "3" в телефонах
def test_real_world_case_295_should_be_phone(self):
"""Real-world case: '295' только цифры, похож на телефон"""
self.assertTrue(is_query_phone_only('295'))
def test_real_world_full_phone_number(self):
"""Real-world case: полный номер в стандартном формате"""
self.assertTrue(is_query_phone_only('+375 (29) 598-62-62'))