fix mobile and names
This commit is contained in:
1 parent
e3a6d1f9d4
commit
7e5c83bea2
12 files changed
+82792
-6
No files matched your search
@@ -0,0 +1,383 @@
|
||||
"""تستهای دستور ``normalize_customer_sms_phones``.
|
||||
|
||||
همهٔ تستها روی دیتابیس تستی Django اجرا میشوند؛ نه به ``data9/kt.sqlite3`` دست
|
||||
میزنند و نه فایل وضعیت داخل ریشهٔ پروژه مینویسند (مسیر وضعیت موقت است).
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.db.models.query import QuerySet
|
||||
from django.test import TestCase
|
||||
|
||||
from admin_panel.models import Customer, PhoneNumber
|
||||
from admin_panel.management.commands.normalize_customer_sms_phones import (
|
||||
FIELD_MAX_LENGTH, digit_string, is_landline)
|
||||
from admin_panel.sms.recipients import resolve_recipients
|
||||
|
||||
from .factories import make_customer
|
||||
|
||||
|
||||
def make_phone(customer, number, title='تلفن'):
|
||||
return PhoneNumber.objects.create(
|
||||
customer=customer, title=title, phone_number=number)
|
||||
|
||||
|
||||
class PureHelperTests(TestCase):
|
||||
"""توابع کمکی اعتبارسنجی/تفکیک (بدون دیتابیس واقعی)."""
|
||||
|
||||
def test_digit_string_converts_persian_and_arabic_digits(self):
|
||||
self.assertEqual(digit_string('۰۹۱۲-۳۴۵'), '0912345')
|
||||
self.assertEqual(digit_string('٠٩١٢ ٣٤٥'), '0912345')
|
||||
self.assertEqual(digit_string('(0912) 345'), '0912345')
|
||||
self.assertEqual(digit_string(None), '')
|
||||
self.assertEqual(digit_string(' '), '')
|
||||
|
||||
def test_landline_detection(self):
|
||||
for raw in ('02112345678', '۰۲۱۱۲۳۴۵۶۷۸', '031-33445566', '05138490000',
|
||||
'57387608', '57473005'): # ۸رقمی = ثابت محلی
|
||||
self.assertTrue(is_landline(raw), msg=raw)
|
||||
for raw in ('09123456789', '9123456789', '12345', '', None,
|
||||
'+989123456789', '0012345678'):
|
||||
self.assertFalse(is_landline(raw), msg=repr(raw))
|
||||
|
||||
|
||||
class CommandTestCase(TestCase):
|
||||
"""زیرساخت مشترک: دیتابیس تستی + فایل وضعیت موقت."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.tmpdir = tempfile.mkdtemp(prefix='smsnorm-')
|
||||
self.addCleanup(shutil.rmtree, self.tmpdir, ignore_errors=True)
|
||||
self.checkpoint = Path(self.tmpdir) / '.sms_normalization_checkpoint.json'
|
||||
|
||||
def run_cmd(self, **options):
|
||||
options.setdefault('checkpoint', str(self.checkpoint))
|
||||
options.setdefault('stdout', io.StringIO())
|
||||
call_command('normalize_customer_sms_phones', **options)
|
||||
return options['stdout'].getvalue()
|
||||
|
||||
def stored(self):
|
||||
return dict(Customer.objects.values_list('id', 'sms_primary_phones'))
|
||||
|
||||
|
||||
class DryRunTests(CommandTestCase):
|
||||
"""پیشفرض: هیچ دیتایی در دیتابیس یا فایل وضعیت نوشته نمیشود."""
|
||||
|
||||
def test_dry_run_writes_nothing(self):
|
||||
customer = make_customer('خشکی')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', sms_primary_phones='+989120000009')
|
||||
make_phone(customer, '09120000002')
|
||||
|
||||
out = self.run_cmd()
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '+989120000009')
|
||||
self.assertFalse(self.checkpoint.exists())
|
||||
self.assertIn('dry-run', out)
|
||||
self.assertIn('قابلبهروزرسانی', out)
|
||||
|
||||
def test_dry_run_reports_counts(self):
|
||||
customer = make_customer('گزارش')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1='09120000001')
|
||||
PhoneNumber.objects.create(
|
||||
customer=customer, title='t', phone_number='02112345678')
|
||||
out = self.run_cmd()
|
||||
self.assertIn('مشتریان بررسیشده: 1', out)
|
||||
self.assertIn('شمارههای فیلترشده: ثابت=1', out)
|
||||
def test_dry_run_flag_conflicts_with_apply(self):
|
||||
customer = make_customer('تناقض فلگ')
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_cmd(dry_run=True, apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '')
|
||||
|
||||
def test_dry_run_ignores_reset_checkpoint(self):
|
||||
self.checkpoint.write_text(json.dumps({'last_id': 5}), encoding='utf-8')
|
||||
self.run_cmd(reset_checkpoint=True)
|
||||
self.assertTrue(self.checkpoint.exists())
|
||||
|
||||
|
||||
class ApplyTests(CommandTestCase):
|
||||
"""اجرای واقعی: تجمیع، حذف تکراری، فیلتر نامعتبر، ایمنی داده."""
|
||||
|
||||
def test_aggregates_all_sources_in_priority_order(self):
|
||||
customer = make_customer('چند منبع')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', phone2='09120000002', phone3='09120000003')
|
||||
PhoneNumber.objects.create(customer=customer, title='a', phone_number='09120000004')
|
||||
PhoneNumber.objects.create(customer=customer, title='b', phone_number='09120000005')
|
||||
|
||||
self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(
|
||||
customer.sms_primary_phones,
|
||||
'+989120000001,+989120000002,+989120000003,+989120000004,+989120000005',
|
||||
)
|
||||
|
||||
def test_dedupes_preserving_priority_order(self):
|
||||
customer = make_customer('تکراری')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', phone2='989120000001') # همان شماره
|
||||
PhoneNumber.objects.create(
|
||||
customer=customer, title='a', phone_number='+98 912 000 0002')
|
||||
PhoneNumber.objects.create(
|
||||
customer=customer, title='b', phone_number='۰۹۱۲۰۰۰۰۰۰۲') # تکراری
|
||||
|
||||
out = self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones,
|
||||
'+989120000001,+989120000002')
|
||||
self.assertIn('شمارههای تکراری حذفشده: 2', out)
|
||||
|
||||
def test_converts_international_and_local_variants(self):
|
||||
customer = make_customer('قالبها')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1='+989120000001')
|
||||
PhoneNumber.objects.create(customer=customer, title='a', phone_number='00989120000002')
|
||||
PhoneNumber.objects.create(customer=customer, title='b', phone_number='989120000003')
|
||||
PhoneNumber.objects.create(customer=customer, title='c', phone_number='9120000004')
|
||||
PhoneNumber.objects.create(customer=customer, title='d', phone_number='0912-000-0005')
|
||||
|
||||
self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(
|
||||
customer.sms_primary_phones,
|
||||
'+989120000001,+989120000002,+989120000003,+989120000004,+989120000005',
|
||||
)
|
||||
|
||||
def test_filters_landlines_and_invalid_values(self):
|
||||
customer = make_customer('نامعتبر')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', phone2='02112345678', phone3='0')
|
||||
PhoneNumber.objects.create(customer=customer, title='a', phone_number='57387608')
|
||||
PhoneNumber.objects.create(customer=customer, title='b', phone_number='اعظمی')
|
||||
PhoneNumber.objects.create(customer=customer, title='c', phone_number='')
|
||||
|
||||
out = self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '+989120000001')
|
||||
self.assertIn('ثابت=2', out) # 02112345678 + 57387608
|
||||
self.assertIn('نامعتبر=2', out) # '0' + 'اعظمی' (ردیف خالی شمرده نمیشود)
|
||||
|
||||
def test_source_fields_and_phone_rows_are_untouched(self):
|
||||
customer = make_customer('ایزوله', address='تهران')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', phone2='02112345678')
|
||||
row = make_phone(customer, '09120000002', title='موبایل')
|
||||
|
||||
self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
row.refresh_from_db()
|
||||
self.assertEqual(customer.phone1, '09120000001')
|
||||
self.assertEqual(customer.phone2, '02112345678')
|
||||
self.assertEqual(customer.name_family, 'ایزوله')
|
||||
self.assertEqual(customer.address, 'تهران')
|
||||
self.assertEqual(row.phone_number, '09120000002')
|
||||
self.assertEqual(row.title, 'موبایل')
|
||||
self.assertEqual(PhoneNumber.objects.filter(customer=customer).count(), 1)
|
||||
|
||||
def test_unchanged_rows_are_not_rewritten(self):
|
||||
customer = make_customer('بدون تغییر')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', sms_primary_phones='+989120000001')
|
||||
|
||||
writes = []
|
||||
original = QuerySet.bulk_update
|
||||
|
||||
def spy(self, objs, fields, batch_size=None):
|
||||
writes.append(len(objs))
|
||||
return original(self, objs, fields, batch_size)
|
||||
|
||||
with mock.patch.object(QuerySet, 'bulk_update', spy):
|
||||
out = self.run_cmd(apply=True)
|
||||
self.assertEqual(writes, []) # هیچ نوشتنی رخ نداد
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '+989120000001')
|
||||
self.assertIn('بدون تغییر: 1', out)
|
||||
|
||||
def test_empty_result_never_erases_existing_value(self):
|
||||
customer = make_customer('محافظت')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='02112345678', sms_primary_phones='+989120000009')
|
||||
|
||||
out = self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '+989120000009')
|
||||
self.assertIn('محافظت شد', out)
|
||||
|
||||
def test_idempotent_second_run_updates_nothing(self):
|
||||
customer = make_customer('تکرار اجرا')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1='09120000001')
|
||||
|
||||
self.run_cmd(apply=True)
|
||||
first = self.stored()
|
||||
out = self.run_cmd(apply=True, reset_checkpoint=True)
|
||||
self.assertEqual(self.stored(), first)
|
||||
self.assertIn('مشتریان بهروزشده: 0', out)
|
||||
|
||||
|
||||
|
||||
class OverflowTests(CommandTestCase):
|
||||
"""سرریز طول: truncate نمیشود، رکورد دستنخورده میماند و در گزارش میآید."""
|
||||
|
||||
def test_overflow_leaves_customer_untouched(self):
|
||||
numbers = ['09120000%03d' % i for i in range(1, 11)] # 10×13 = 139 > 100
|
||||
customer = make_customer('سرریز')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1=numbers[0], phone2=numbers[1])
|
||||
for number in numbers[2:]:
|
||||
PhoneNumber.objects.create(customer=customer, title='t', phone_number=number)
|
||||
|
||||
out = self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.sms_primary_phones, '')
|
||||
self.assertIn(f'سرریز طول (>{FIELD_MAX_LENGTH} کاراکتر، دستنخورده): 1', out)
|
||||
self.assertIn(f'شناسههای سرریز: {customer.id}', out)
|
||||
|
||||
def test_exactly_under_max_length_is_saved(self):
|
||||
# 7 شماره × 13 کاراکتر + 6 کاما = 97 ≤ 100
|
||||
numbers = ['09120000%03d' % i for i in range(1, 8)]
|
||||
customer = make_customer('لبه طول')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1=numbers[0])
|
||||
for number in numbers[1:]:
|
||||
PhoneNumber.objects.create(customer=customer, title='t', phone_number=number)
|
||||
|
||||
out = self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(len(customer.sms_primary_phones), 97)
|
||||
self.assertEqual(customer.sms_primary_phones.count(','), 6)
|
||||
self.assertIn('سرریز طول', out)
|
||||
|
||||
|
||||
class BatchAndCheckpointTests(CommandTestCase):
|
||||
"""keyset pagination، batch اتمیک، و بازیابی از فایل وضعیت."""
|
||||
|
||||
def _customers_with_phone(self, count):
|
||||
pks = []
|
||||
for i in range(count):
|
||||
customer = make_customer(f'مشتری {i}')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1='09120%06d' % (i + 1))
|
||||
pks.append(customer.pk)
|
||||
return pks
|
||||
|
||||
|
||||
def test_all_customers_covered_with_small_batches(self):
|
||||
pks = self._customers_with_phone(7)
|
||||
out = self.run_cmd(apply=True, batch_size=2)
|
||||
self.assertEqual(
|
||||
Customer.objects.filter(pk__in=pks).exclude(sms_primary_phones='').count(), 7)
|
||||
self.assertIn('batchهای تکمیلشده: 4', out) # ۷ مشتری / ۲ = ۴ batch
|
||||
|
||||
def test_checkpoint_written_after_each_batch(self):
|
||||
self._customers_with_phone(5)
|
||||
self.run_cmd(apply=True, batch_size=2)
|
||||
data = json.loads(self.checkpoint.read_text(encoding='utf-8'))
|
||||
self.assertEqual(data['last_id'], Customer.objects.order_by('-id').first().pk)
|
||||
self.assertEqual(data['batches'], 3)
|
||||
self.assertTrue(data['applied'])
|
||||
|
||||
def test_resume_continues_from_checkpoint(self):
|
||||
pks = self._customers_with_phone(4)
|
||||
self.checkpoint.write_text(json.dumps({'last_id': pks[1]}), encoding='utf-8')
|
||||
|
||||
self.run_cmd(apply=True, resume=True)
|
||||
self.assertEqual(Customer.objects.get(pk=pks[0]).sms_primary_phones, '')
|
||||
self.assertEqual(
|
||||
Customer.objects.filter(pk__in=pks[2:]).exclude(sms_primary_phones='').count(), 2)
|
||||
|
||||
def test_resume_without_checkpoint_fails(self):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_cmd(apply=True, resume=True)
|
||||
|
||||
def test_resume_with_invalid_checkpoint_fails(self):
|
||||
self.checkpoint.write_text('{not json', encoding='utf-8')
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_cmd(apply=True, resume=True)
|
||||
|
||||
def test_start_id_takes_precedence(self):
|
||||
pks = self._customers_with_phone(4)
|
||||
self.checkpoint.write_text(json.dumps({'last_id': 0}), encoding='utf-8')
|
||||
self.run_cmd(apply=True, resume=True, start_id=pks[2])
|
||||
self.assertEqual(Customer.objects.get(pk=pks[0]).sms_primary_phones, '')
|
||||
self.assertEqual(Customer.objects.get(pk=pks[3]).sms_primary_phones,
|
||||
'+989120000004')
|
||||
|
||||
def test_reset_checkpoint_restarts_from_beginning(self):
|
||||
self._customers_with_phone(3)
|
||||
self.run_cmd(apply=True, batch_size=1)
|
||||
self.checkpoint.write_text(json.dumps({'last_id': 10**9}), encoding='utf-8')
|
||||
self.run_cmd(apply=True, reset_checkpoint=True, batch_size=1)
|
||||
data = json.loads(self.checkpoint.read_text(encoding='utf-8'))
|
||||
self.assertEqual(data['batches'], 3) # از ابتدا شروع شد
|
||||
|
||||
def test_batch_failure_keeps_previous_batches_and_checkpoint(self):
|
||||
pks = self._customers_with_phone(3)
|
||||
original = QuerySet.bulk_update
|
||||
calls = iter([1, 2])
|
||||
|
||||
def flaky(self, objs, fields, batch_size=None):
|
||||
if next(calls, 2) == 2:
|
||||
raise RuntimeError('boom')
|
||||
return original(self, objs, fields, batch_size)
|
||||
|
||||
with mock.patch.object(QuerySet, 'bulk_update', flaky):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_cmd(apply=True, batch_size=1)
|
||||
|
||||
# batch اول commit شده، batch دوم rollback شده، مشتری سوم دستنخورده
|
||||
self.assertEqual(Customer.objects.get(pk=pks[0]).sms_primary_phones,
|
||||
'+989120000001')
|
||||
self.assertEqual(Customer.objects.get(pk=pks[1]).sms_primary_phones, '')
|
||||
self.assertEqual(Customer.objects.get(pk=pks[2]).sms_primary_phones, '')
|
||||
data = json.loads(self.checkpoint.read_text(encoding='utf-8'))
|
||||
self.assertEqual(data['last_id'], pks[0])
|
||||
|
||||
def test_resume_after_failure_completes_remaining(self):
|
||||
pks = self._customers_with_phone(3)
|
||||
original = QuerySet.bulk_update
|
||||
calls = iter([1, 2])
|
||||
|
||||
def flaky(self, objs, fields, batch_size=None):
|
||||
if next(calls, 2) == 2:
|
||||
raise RuntimeError('boom')
|
||||
return original(self, objs, fields, batch_size)
|
||||
|
||||
with mock.patch.object(QuerySet, 'bulk_update', flaky):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_cmd(apply=True, batch_size=1)
|
||||
|
||||
self.run_cmd(apply=True, resume=True, batch_size=1)
|
||||
for pk in pks:
|
||||
self.assertNotEqual(Customer.objects.get(pk=pk).sms_primary_phones, '')
|
||||
|
||||
|
||||
class IntegrationAndEfficiencyTests(CommandTestCase):
|
||||
"""سازگاری با مسیر پیامک موجود + جلوگیری از N+1."""
|
||||
|
||||
def test_panel_resolution_reads_back_aggregated_numbers(self):
|
||||
customer = make_customer('گیرنده')
|
||||
Customer.objects.filter(pk=customer.pk).update(
|
||||
phone1='09120000001', phone2='09120000002')
|
||||
self.run_cmd(apply=True)
|
||||
customer.refresh_from_db()
|
||||
# قالب ذخیرهشده باید همان چیزی باشد که مسیر پیامک انتظار دارد.
|
||||
self.assertEqual(resolve_recipients(customer),
|
||||
['+989120000001', '+989120000002'])
|
||||
|
||||
def test_query_count_is_constant_per_batch(self):
|
||||
for i in range(3):
|
||||
customer = make_customer(f'کوئری {i}')
|
||||
Customer.objects.filter(pk=customer.pk).update(phone1='09120%06d' % (i + 1))
|
||||
for j in range(5):
|
||||
PhoneNumber.objects.create(
|
||||
customer=customer, title='t',
|
||||
phone_number='09130%06d' % (i * 10 + j))
|
||||
|
||||
with self.assertNumQueries(3): # ۱ کوئری سقف دامنه + ۱ مشتریان + ۱ prefetch
|
||||
self.run_cmd()
|
||||
|
||||
Reference in new issue
Block a user