forked from erp-dev/erp
feat: error_code + api doc + printing-job fields
This commit is contained in:
264
api_v2/test_auth_login_api.py
Normal file
264
api_v2/test_auth_login_api.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from basic_info.models import Employee, EmployeeStatusEnum, Merchant, MerchantTypeEnum
|
||||
from flower.error_code import AuthErrorCode
|
||||
|
||||
|
||||
class AuthLoginAPITestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.url = '/api/auth/login/'
|
||||
self.client = APIClient()
|
||||
self.merchant = Merchant.objects.create(name='登录商户', type=MerchantTypeEnum.FACTORY)
|
||||
|
||||
def _create_user_with_employee(self, *, username='login_user', password='pass12345', is_active=True,
|
||||
employee_status=EmployeeStatusEnum.ACTIVE):
|
||||
user = User.objects.create_user(username=username, password=password, is_active=is_active)
|
||||
Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=user,
|
||||
name=f'{username}员工',
|
||||
status=employee_status,
|
||||
)
|
||||
return user
|
||||
|
||||
def assert_login_error(self, response, *, http_status, error_code, code, message):
|
||||
self.assertEqual(response.status_code, http_status)
|
||||
self.assertEqual(response.data['error_code'], int(error_code))
|
||||
self.assertEqual(response.data['code'], code)
|
||||
self.assertEqual(response.data['message'], message)
|
||||
self.assertEqual(response.data['detail'], message)
|
||||
|
||||
def test_login_success(self):
|
||||
self._create_user_with_employee(username='success_user', password='pass12345')
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'success_user', 'password': 'pass12345'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('access', response.data)
|
||||
self.assertIn('refresh', response.data)
|
||||
|
||||
def test_login_requires_username_and_password(self):
|
||||
response = self.client.post(self.url, {}, format='json')
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_400_BAD_REQUEST,
|
||||
error_code=AuthErrorCode.MISSING_CREDENTIALS,
|
||||
code='missing_credentials',
|
||||
message='请输入用户名和密码',
|
||||
)
|
||||
|
||||
def test_login_requires_username(self):
|
||||
response = self.client.post(self.url, {'password': 'pass12345'}, format='json')
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_400_BAD_REQUEST,
|
||||
error_code=AuthErrorCode.MISSING_USERNAME,
|
||||
code='missing_username',
|
||||
message='请输入用户名',
|
||||
)
|
||||
|
||||
def test_login_requires_password(self):
|
||||
response = self.client.post(self.url, {'username': 'login_user'}, format='json')
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_400_BAD_REQUEST,
|
||||
error_code=AuthErrorCode.MISSING_PASSWORD,
|
||||
code='missing_password',
|
||||
message='请输入密码',
|
||||
)
|
||||
|
||||
def test_login_rejects_unknown_user(self):
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'missing_user', 'password': 'pass12345'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.USER_NOT_FOUND,
|
||||
code='user_not_found',
|
||||
message='用户不存在',
|
||||
)
|
||||
|
||||
def test_login_rejects_invalid_password(self):
|
||||
self._create_user_with_employee(username='wrong_password_user', password='pass12345')
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'wrong_password_user', 'password': 'bad-password'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.INVALID_PASSWORD,
|
||||
code='invalid_password',
|
||||
message='密码错误',
|
||||
)
|
||||
|
||||
def test_login_rejects_inactive_user(self):
|
||||
self._create_user_with_employee(username='inactive_user', password='pass12345', is_active=False)
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'inactive_user', 'password': 'pass12345'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_403_FORBIDDEN,
|
||||
error_code=AuthErrorCode.USER_INACTIVE,
|
||||
code='user_inactive',
|
||||
message='该用户已被禁用',
|
||||
)
|
||||
|
||||
def test_login_rejects_user_without_employee(self):
|
||||
User.objects.create_user(username='no_employee_user', password='pass12345')
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'no_employee_user', 'password': 'pass12345'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_403_FORBIDDEN,
|
||||
error_code=AuthErrorCode.EMPLOYEE_NOT_BOUND,
|
||||
code='employee_not_bound',
|
||||
message='该用户未绑定员工身份',
|
||||
)
|
||||
|
||||
def test_login_rejects_inactive_employee(self):
|
||||
self._create_user_with_employee(
|
||||
username='inactive_employee_user',
|
||||
password='pass12345',
|
||||
employee_status=EmployeeStatusEnum.INACTIVE,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{'username': 'inactive_employee_user', 'password': 'pass12345'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assert_login_error(
|
||||
response,
|
||||
http_status=status.HTTP_403_FORBIDDEN,
|
||||
error_code=AuthErrorCode.EMPLOYEE_INACTIVE,
|
||||
code='employee_inactive',
|
||||
message='该员工已离职或停用',
|
||||
)
|
||||
|
||||
|
||||
class JwtAuthenticationFailureAPITestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.url = '/api/v2/me/visible-pages/'
|
||||
self.client = APIClient()
|
||||
|
||||
def assert_auth_error(self, response, *, http_status, error_code, code, message):
|
||||
self.assertEqual(response.status_code, http_status)
|
||||
self.assertEqual(response.data['error_code'], int(error_code))
|
||||
self.assertEqual(response.data['code'], code)
|
||||
self.assertEqual(response.data['message'], message)
|
||||
self.assertEqual(response.data['detail'], message)
|
||||
|
||||
def test_protected_api_without_token_returns_structured_error(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assert_auth_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.NOT_AUTHENTICATED,
|
||||
code='not_authenticated',
|
||||
message='未提供认证凭据',
|
||||
)
|
||||
|
||||
def test_protected_api_bad_authorization_header_returns_structured_error(self):
|
||||
response = self.client.get(self.url, HTTP_AUTHORIZATION='Bearer token extra')
|
||||
|
||||
self.assert_auth_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.BAD_AUTHORIZATION_HEADER,
|
||||
code='bad_authorization_header',
|
||||
message='Authorization 请求头格式错误',
|
||||
)
|
||||
|
||||
def test_protected_api_invalid_token_returns_structured_error(self):
|
||||
response = self.client.get(self.url, HTTP_AUTHORIZATION='Bearer invalid-token')
|
||||
|
||||
self.assert_auth_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.TOKEN_NOT_VALID,
|
||||
code='token_not_valid',
|
||||
message='Token 无效或已过期',
|
||||
)
|
||||
self.assertIn('messages', response.data)
|
||||
|
||||
def test_protected_api_deleted_token_user_returns_structured_error(self):
|
||||
user = User.objects.create_user(username='deleted_token_user', password='pass12345')
|
||||
token = str(RefreshToken.for_user(user).access_token)
|
||||
user.delete()
|
||||
|
||||
response = self.client.get(self.url, HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||
|
||||
self.assert_auth_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.TOKEN_USER_NOT_FOUND,
|
||||
code='token_user_not_found',
|
||||
message='Token 对应用户不存在',
|
||||
)
|
||||
|
||||
def test_protected_api_inactive_token_user_returns_structured_error(self):
|
||||
user = User.objects.create_user(username='inactive_token_user', password='pass12345', is_active=False)
|
||||
token = str(RefreshToken.for_user(user).access_token)
|
||||
|
||||
response = self.client.get(self.url, HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||
|
||||
self.assert_auth_error(
|
||||
response,
|
||||
http_status=status.HTTP_401_UNAUTHORIZED,
|
||||
error_code=AuthErrorCode.TOKEN_USER_INACTIVE,
|
||||
code='token_user_inactive',
|
||||
message='Token 对应用户已被禁用',
|
||||
)
|
||||
|
||||
|
||||
class ErrorCodeListAPITestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.url = '/api/error-codes/'
|
||||
self.client = APIClient()
|
||||
|
||||
def test_error_code_list_is_public_and_includes_auth_codes(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
auth_group = next(group for group in response.data['modules'] if group['module'] == 'auth')
|
||||
self.assertEqual(auth_group['title'], '认证')
|
||||
|
||||
codes = {item['error_code']: item for item in auth_group['codes']}
|
||||
self.assertEqual(codes[10001]['name'], 'MISSING_CREDENTIALS')
|
||||
self.assertEqual(codes[10001]['code'], 'missing_credentials')
|
||||
self.assertEqual(codes[10001]['message'], '请输入用户名和密码')
|
||||
self.assertEqual(codes[10014]['name'], 'PERMISSION_DENIED')
|
||||
self.assertEqual(codes[10014]['code'], 'permission_denied')
|
||||
self.assertEqual(codes[10014]['message'], '无权限访问该资源')
|
||||
self.assertEqual(set(codes.keys()), {int(item) for item in AuthErrorCode})
|
||||
@@ -46,11 +46,13 @@ class PrintingOrderExternalSnapshotSyncAPITest(TestCase):
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name='Tj1712#12号色-24码',
|
||||
image='product_images/keep.jpg',
|
||||
)
|
||||
self.product_stale = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name='Tj1712#12号色-25码',
|
||||
image='product_images/stale.jpg',
|
||||
)
|
||||
|
||||
state = stateflow_models.State.objects.create(name='待生产')
|
||||
@@ -233,6 +235,39 @@ class PrintingOrderExternalSnapshotSyncAPITest(TestCase):
|
||||
self.assertEqual(len(audit.before_snapshot['printing_jobs']), 2)
|
||||
self.assertIn('sub_id', audit.before_snapshot['printing_jobs'][0])
|
||||
|
||||
@patch('api_v1.tasks._fetch_external_product_image')
|
||||
@patch('api_v1.tasks._fetch_external_printing_order_snapshot')
|
||||
def test_sync_allows_missing_image_for_white_fabric_product(self, mock_fetch_snapshot, mock_fetch_image):
|
||||
product_name = '外部白布-快照款'
|
||||
mock_fetch_snapshot.return_value = self._build_snapshot_payload(
|
||||
records=[
|
||||
self._build_record(
|
||||
record_id=1000018,
|
||||
product_name=product_name,
|
||||
quantity='12.00',
|
||||
)
|
||||
]
|
||||
)
|
||||
mock_fetch_image.side_effect = RuntimeError(f'外部图片不存在: {product_name}')
|
||||
|
||||
resp = self.client.post(
|
||||
'/api/v2/printing-orders/sync-external-snapshot/',
|
||||
{'external_order_id': 'KD20432358'},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data['jobs_created'], 1)
|
||||
|
||||
product = basic_models.Product.objects.get(merchant=self.merchant, name=product_name)
|
||||
self.assertFalse(product.image)
|
||||
job = printing_models.PrintingJob.objects.get(original_id=1000018)
|
||||
self.assertEqual(job.product, product)
|
||||
|
||||
audit = printing_models.PrintingOrderExternalSnapshotSyncAudit.objects.get(id=resp.data['audit_id'])
|
||||
self.assertTrue(audit.is_success)
|
||||
self.assertEqual(audit.failure_reason, '')
|
||||
|
||||
@patch('api_v1.tasks._fetch_external_printing_order_snapshot')
|
||||
def test_sync_records_audit_when_external_snapshot_not_found(self, mock_fetch_snapshot):
|
||||
mock_fetch_snapshot.side_effect = ExternalPrintingOrderSnapshotNotFoundError('未找到该 external_order_id 对应的订单')
|
||||
|
||||
@@ -171,6 +171,8 @@ class PrintingJobByCustomerAPITest(TestCase):
|
||||
customer=self.customer,
|
||||
fabric='棉',
|
||||
width='150cm',
|
||||
curve='曲线A',
|
||||
new_curve='新曲线A',
|
||||
external_order_id='KD20410611',
|
||||
)
|
||||
self.printing_order_other = printing_models.PrintingOrder.objects.create(
|
||||
@@ -256,6 +258,8 @@ class PrintingJobByCustomerAPITest(TestCase):
|
||||
self.assertEqual(resp.data[0]['external_order_id'], 'KD20410611')
|
||||
self.assertEqual(resp.data[0]['sub_id'], 11)
|
||||
self.assertEqual(resp.data[0]['billed_quantity'], '20.00')
|
||||
self.assertEqual(resp.data[0]['curve'], '曲线A')
|
||||
self.assertEqual(resp.data[0]['new_curve'], '新曲线A')
|
||||
|
||||
def test_filter_by_printing_order(self):
|
||||
resp = self._get({
|
||||
|
||||
@@ -55,6 +55,8 @@ class PrintingJobV2Serializer(serializers.ModelSerializer):
|
||||
business_object_id = serializers.SerializerMethodField()
|
||||
width = serializers.SerializerMethodField()
|
||||
fabric = serializers.SerializerMethodField()
|
||||
curve = serializers.CharField(source='printing_order.curve', read_only=True, allow_null=True)
|
||||
new_curve = serializers.CharField(source='printing_order.new_curve', read_only=True, allow_null=True)
|
||||
product = ProductSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -70,6 +72,8 @@ class PrintingJobV2Serializer(serializers.ModelSerializer):
|
||||
'quantity',
|
||||
'width',
|
||||
'fabric',
|
||||
'curve',
|
||||
'new_curve',
|
||||
'unit',
|
||||
'size',
|
||||
'pieces',
|
||||
|
||||
Reference in New Issue
Block a user