230 lines
12 KiB
Python
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}"
|
|
),
|
|
)
|