Files
khatibi/tests/test_view_smoke.py
T
2026-10-09 20:02:36 +03:30

230 lines
12 KiB
Python

"""
تست دودی (Smoke Test) تمام اندپوینت‌های اپ admin_panel.
چرا این تست حیاتی است؟
`python manage.py check` خطاهای `NameError`/`ImportError` را که داخل *بدنهٔ توابع*
ویو رخ می‌دهند نمی‌بیند. پس از تقسیم `views.py` به پکیج `views/`، تنها راه مطمئن
برای کشف importهای جاافتاده، فراخوانی واقعی هر ویو است. این ماژول دقیقاً همان کار
را انجام می‌دهد: هر مسیر urls.py با یک کاربر مجاز (superuser) فراخوانی می‌شود و
هیچ پاسخی نباید کد ۵۰۰ داشته باشد.
"""
import json
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from admin_panel.models import (Action, Article, Factor, Sarresid,
StoreTransferRequest)
from .factories import (make_category, make_customer, make_product, make_store,
make_user, stock_in)
# کدهای وضعیتی که «نتیجهٔ قابل‌قبول» تلقی می‌شوند (نشانهٔ اجرای سالم ویو)
ACCEPTABLE = {200, 201, 302, 400, 403, 404, 405}
GET = 'get'
POST = 'post'
class ViewSmokeTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = make_user('smokeAdmin', superuser=True)
cls.other_user = make_user('smokeOther')
# انبار اصلی که کاربر مالک آن است (owners) و دسترسی مشاهده دارد
cls.store = make_store('انبار اصلی', owner=cls.user)
# نکته: کاربر عمداً به انبار دوم اضافه نمی‌شود؛ چون چند ویو از
# `StoreRoom.objects.get(user__id=...)` استفاده می‌کنند که با دو انبار
# خطای MultipleObjectsReturned می‌دهد (رفتار پیشین کد، نه ناشی از refactor).
cls.store2 = make_store('انبار دوم')
cls.category = make_category('دسته اصلی')
cls.product = make_product('کالای اصلی', price=2000, category=cls.category)
cls.spare_product = make_product('کالای حذفی', price=1500, category=cls.category)
stock_in(cls.store, cls.product, 100)
stock_in(cls.store, cls.spare_product, 5)
cls.customer = make_customer('مشتری اصلی')
cls.spare_customer = make_customer('مشتری خام')
# یک سند مالی تا ویو «بدهی» مسیر موفق را طی کند (با دیتابیس کاملاً خالی،
# این ویو به دلیل باگ ازپیش‌موجود UnboundLocalError خطای ۵۰۰ می‌دهد).
Article.objects.create(customer=cls.customer, debit=1000, credit=0, desc='سند آغازین دودی')
cls.factor = Factor.objects.create(customer=cls.customer, amount=1000, pay_status='unpaid')
cls.spare_factor = Factor.objects.create(customer=cls.customer, amount=1000, pay_status='unpaid')
cls.action = Action.objects.create(
customer=cls.customer, forUser=cls.user, actionType='bazdid', status='waite',
actionDateTime=timezone.now(),
)
cls.sarresid = Sarresid.objects.create(
customer=cls.customer, price=5000, payStatus='false', type='check',
sarresidDT=timezone.now(),
)
cls.transfer = StoreTransferRequest.objects.create(
source_store=cls.store, target_store=cls.store2,
product=cls.product, quantity=1, requester=cls.user,
)
def setUp(self):
self.client.force_login(self.user)
# در صورت خطای سرور، استثنا را نگه می‌داریم تا کد وضعیت ۵۰۰ را ببینیم
self.client.raise_request_exception = False
# ------------------------------------------------------------------ helpers
def _now_ms(self):
return int(timezone.now().timestamp() * 1000)
def _url(self, name, **kwargs):
return reverse(f'admin_panel:{name}', kwargs=kwargs)
def _endpoints(self):
"""(نام مسیر, kwargs, متد, دادهٔ ارسالی)"""
ms = self._now_ms()
c = self.customer
return [
# --- داشبوردها و گزارش‌ها ---
('index', {}, GET, None),
('p_login', {}, GET, None),
('bedehi', {}, GET, None),
('report', {}, GET, None),
('inventory_movements_report', {}, GET, None),
('financial_dashboard', {}, GET, None),
('report_debtors_no_vosol', {}, GET, None),
('rezayat_dashboard', {}, GET, None),
('hesabdari', {}, GET, None),
('hesabdari_residnashone', {}, GET, None),
('hesabdari_check_pass', {}, GET, None),
('hesabdari_check_pass_fail', {}, GET, None),
('bedehiKhadamat', {}, GET, None),
('bedehiNasb', {}, GET, None),
# --- مشتریان ---
('customers', {}, GET, None),
# مسیر از converter نوع <slug> استفاده می‌کند، پس فقط کاراکترهای مجاز slug
('customersSearch', {'searchVal': 'ali', 'searchOn': 'name'}, GET, None),
('customersAdd', {}, POST, {
'an_name': 'مشتری دودی', 'an_address': 'تهران',
'installDate': '1400/01/01', 'idnCode': '111',
'phone_titles': ['موبایل'], 'phones': ['09120000000'],
}),
('customer', {'c_id': c.id}, GET, None),
('customer2', {'c_id': c.id}, GET, None),
('customerNew', {'c_id': c.id}, GET, None),
('customerAction', {'c_id': c.id}, GET, None),
('customerNazar', {'c_id': c.id}, GET, None),
('customerNewAction', {'c_id': c.id}, POST, {
'actionType': 'bazdid', 'dateTimeAnjam': ms,
'userID': self.user.id, 'desc': 'اقدام دودی',
}),
('customerEditAction', {'c_id': c.id, 'action_id': self.action.id}, POST, {
'actionType': 'bazdid', 'dateTimeAnjam': ms,
'userID': self.user.id, 'desc': 'ویرایش دودی',
}),
('customerDel', {'c_id': self.spare_customer.id}, GET, None),
('customerFactors', {'c_id': c.id}, GET, None),
('customerFactor', {'c_id': c.id, 'f_id': self.factor.id}, GET, None),
('FactorDel', {'c_id': c.id, 'f_id': self.spare_factor.id}, POST, {
'cancel_reason': 'error', 'payment_action': 'keep',
}),
('newFactor', {'c_id': c.id}, POST, {
'prdsList': json.dumps([]), 'payType': 'naghd',
'FactorType': 'Nasb', 'FactorDesc': 'دودی', 'sarResidDate': '',
}),
('newFactor2', {'c_id': c.id}, POST, {
'prdsList': json.dumps([]), 'PardakhtHa': json.dumps({}),
'FactorType': 'Nasb', 'FactorDesc': 'دودی', 'sarResidDate': '',
}),
('customerHesabdari', {'c_id': c.id}, GET, None),
('customerSarresidh', {'c_id': c.id}, GET, None),
('customerHesabdariAdd', {'c_id': c.id}, POST, {
'price': '1000', 'type': 'debit', 'typePy': 'nasb',
'desc': 'سند دودی', 'nahvePardakhat': 'نقد',
}),
# --- JSON ---
('productsJson', {}, GET, None),
('ActionJson', {'action_id': self.action.id}, GET, None),
('CustomerJson', {'c_id': c.id}, GET, None),
('productMojodi', {'p_id': self.product.id}, GET, None),
# --- انبار ---
('stores', {}, GET, None),
('store', {'s_id': self.store.id}, GET, None),
('operations', {'s_id': self.store.id}, GET, None),
('operationProduct', {'s_id': self.store.id, 'p_id': self.product.id}, GET, None),
('newOperation', {'s_id': self.store.id, 'p_id': self.product.id}, GET, None),
('newStore', {}, POST, {'storeName': 'انبار دودی'}),
('store_transfers', {'s_id': self.store.id}, GET, None),
('process_transfer_action', {'transfer_id': self.transfer.id}, POST, {'action': 'invalid'}),
('productAdd', {}, POST, {
'namePrd': 'کالای دودی', 'category': self.category.id,
'price': '1000', 'mojodiAval': '5', 'storeroom': self.store.id,
}),
('productEdit', {}, POST, {
'prd_code': self.product.id, 'namePrd': 'کالای ویرایش‌شده',
'category': self.category.id, 'price': '2500',
}),
('operation', {}, POST, {
'prd_id': self.product.id, 'actionType': 'vorod',
'tedad': '3', 'azanbar': self.store.id, 'desc': 'ورود دودی',
}),
('productDel', {'p_id': self.spare_product.id, 's_id': self.store.id}, GET, None),
('customerEdit', {'c_id': c.id}, POST, {
'an_name': 'مشتری ویرایش‌دودی', 'an_address': 'کرج',
'installDate': '1400/02/02', 'idnCode': '222',
'phone_titles': ['موبایل'], 'phones': ['09120000001'],
}),
# --- اقدامات و نظرسنجی ---
('rezayat', {}, GET, None),
('rezayatEdit', {'ac_id': self.action.id}, POST, {'csGrade': '20', 'csDesc': 'عالی'}),
('entezar', {}, GET, None),
('ActionList', {}, GET, None),
('ActionListToday', {}, GET, None),
('ActionListUserToday', {'u_id': self.user.id}, GET, None),
('ActionListAll', {}, GET, None),
# --- چک و سررسید ---
('passCheck', {'articl_id': self.sarresid.id}, POST, {'message': 'وصول شد'}),
('update_check_info', {'sarresid_id': self.sarresid.id}, POST, {
'check_row_number': '1', 'check_sayyad_id': '1234567890123456',
'check_owner_name': 'مالک', 'check_owner_phone': '09120000000',
}),
('addSarresid', {'cs_id': c.id}, POST, {
'n_sarresidDateTime': ms, 'n_sarresidPrice': '1000',
'n_sarresidType': 'nesiye',
}),
('pardakhtNesiye', {'cs_id': c.id}, POST, {
'next_sarresidDateTime': ms, 'next_sarresidPrice': '100',
'sr_articlID': self.sarresid.id, 'nahvePardakht': 'naghd',
}),
('api_check_status_inquiry', {}, GET, None),
('api_check_sabt_inquiry', {}, GET, None),
# در انتهای لیست: خروج از حساب، چون بعد از آن تمام درخواست‌ها بی‌اعتبار می‌شوند
('p_logout', {}, GET, None),
]
# ------------------------------------------------------------------ tests
def test_every_endpoint_responds_without_server_error(self):
failures = []
for name, kwargs, method, data in self._endpoints():
url = self._url(name, **kwargs)
with self.subTest(endpoint=name):
# HTTP_REFERER لازم است چون چند ویو با redirect(request.META['HTTP_REFERER'])
# بازمی‌گردند و مرورگر واقعی همیشه آن را می‌فرستد.
headers = {'HTTP_REFERER': '/panel/'}
if method == GET:
response = self.client.get(url, **headers)
else:
response = self.client.post(url, data or {}, **headers)
if response.status_code not in ACCEPTABLE:
failures.append((name, response.status_code, url))
self.assertEqual(
failures, [],
msg=(
"این اندپوینت‌ها پاسخ غیرمنتظره (احتمالاً خطای ۵۰۰ ناشی از import جاافتاده) "
f"داده‌اند: {failures}"
),
)