"""CMS 超级管理员 - 用户管理接口测试(CMS-USER-1~8)。""" import io from openpyxl import Workbook from django.core.files.uploadedfile import SimpleUploadedFile from rest_framework.test import APIClient from apps.user.models import User from .conftest import CacheTestCase, create_test_user, get_auth_client, ensure_institution CMS_USER_URL = '/api/cms/users/' XLSX_CT = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' def u_detail(pk): return f'/api/cms/users/{pk}/' def make_xlsx(headers, rows): wb = Workbook(); ws = wb.active ws.append(headers) for r in rows: ws.append(r) buf = io.BytesIO(); wb.save(buf); buf.seek(0) return SimpleUploadedFile('import.xlsx', buf.read(), content_type=XLSX_CT) def super_client(phone='13922200001'): admin = create_test_user(phone=phone, password='Admin123', role_type='super_admin') return get_auth_client(admin), admin class CmsUserPermissionTest(CacheTestCase): def test_requires_auth(self): self.assertEqual(APIClient().get(CMS_USER_URL).status_code, 401) def test_non_manager_forbidden(self): # 用户管理仅超管 + 医院管理员;其它角色 403 for role in ('student', 'doctor', 'content_admin'): u = create_test_user(phone={'student': '13922200010', 'doctor': '13922200011', 'content_admin': '13922200012'}[role], role_type=role) resp = get_auth_client(u).get(CMS_USER_URL) self.assertEqual(resp.status_code, 403, f'{role}: {resp.content}') self.assertEqual(resp.json()['code'], 'CMS_PERMISSION_DENIED') class CmsUserCrudTest(CacheTestCase): def setUp(self): super().setUp() self.client, self.admin = super_client() self.inst = ensure_institution(name='测试医院', code='CU-H001') def test_list(self): create_test_user(phone='13922200021', real_name='学生甲', role_type='student') resp = self.client.get(CMS_USER_URL) self.assertEqual(resp.status_code, 200, resp.content) self.assertIn('results', resp.json()) def test_create_success(self): resp = self.client.post(CMS_USER_URL, { 'phone': '13922200030', 'real_name': '李医生', 'role_type': 'doctor', 'institution': self.inst.id, }) self.assertEqual(resp.status_code, 201, resp.content) self.assertEqual(resp.json()['phone'], '13922200030') self.assertEqual(resp.json()['role_type'], 'doctor') user = User.objects.get(phone='13922200030') self.assertTrue(user.check_password('Pass13922200030')) # 默认密码 def test_create_role_required(self): resp = self.client.post(CMS_USER_URL, { 'phone': '13922200031', 'real_name': '无角色', 'institution': self.inst.id}) self.assertEqual(resp.status_code, 400, resp.content) def test_create_institution_required(self): resp = self.client.post(CMS_USER_URL, { 'phone': '13922200032', 'real_name': '无机构', 'role_type': 'student'}) self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR') def test_create_bad_phone(self): resp = self.client.post(CMS_USER_URL, { 'phone': '123', 'real_name': 'x', 'role_type': 'student', 'institution': self.inst.id}) self.assertEqual(resp.status_code, 400, resp.content) def test_create_duplicate_phone(self): create_test_user(phone='13922200040', role_type='student') resp = self.client.post(CMS_USER_URL, { 'phone': '13922200040', 'real_name': 'dup', 'role_type': 'student', 'institution': self.inst.id}) self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_USER_PHONE_EXISTS') def test_edit_partial_keeps_role_institution(self): """方案B:只改姓名、不带角色/机构 → 200,角色与机构保持原值。""" u = create_test_user(phone='13922200050', real_name='原名', role_type='student', institution=self.inst) resp = self.client.patch(u_detail(u.id), {'real_name': '新名'}) self.assertEqual(resp.status_code, 200, resp.content) u.refresh_from_db() self.assertEqual(u.real_name, '新名') self.assertEqual(u.role_type, 'student') # 未传 → 保持 self.assertEqual(u.institution_id, self.inst.id) # 未传 → 保持 def test_edit_cannot_blank_role(self): """方案B:传了 role_type 但为空 → 400(不可清空角色)。""" u = create_test_user(phone='13922200051', real_name='x', role_type='student', institution=self.inst) resp = self.client.patch(u_detail(u.id), {'role_type': ''}) self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR') def test_edit_cannot_blank_institution(self): """方案B:传了 institution=null → 400(不可清空机构)。""" u = create_test_user(phone='13922200052', real_name='x', role_type='student', institution=self.inst) resp = self.client.patch(u_detail(u.id), {'institution': None}, format='json') self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR') def test_soft_delete(self): u = create_test_user(phone='13922200060', role_type='student') resp = self.client.delete(u_detail(u.id)) self.assertEqual(resp.status_code, 204, resp.content) self.assertFalse(User.objects.filter(id=u.id).exists()) # 默认管理器过滤 obj = User.all_objects.get(id=u.id) self.assertTrue(obj.is_deleted) # 实际未物删 def test_recreate_soft_deleted_phone_returns_400(self): """软删后用相同手机号重建:返回 400 CMS_USER_PHONE_EXISTS(不产生重复行)。""" u = create_test_user(phone='13922200061', role_type='student', institution=self.inst) self.client.delete(u_detail(u.id)) self.assertFalse(User.objects.filter(phone='13922200061').exists()) resp = self.client.post(CMS_USER_URL, { 'phone': '13922200061', 'real_name': '重建', 'role_type': 'student', 'institution': self.inst.id}) self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_USER_PHONE_EXISTS') self.assertEqual(User.all_objects.filter(phone='13922200061').count(), 1) def test_reset_password(self): u = create_test_user(phone='13922200070', password='OldPass1', role_type='student') resp = self.client.post(f'/api/cms/users/{u.id}/reset-password/', {}) self.assertEqual(resp.status_code, 200, resp.content) self.assertEqual(resp.json()['password'], 'Pass13922200070') u.refresh_from_db() self.assertTrue(u.check_password('Pass13922200070')) def test_import_template(self): resp = self.client.get('/api/cms/users/import-template/') self.assertEqual(resp.status_code, 200, resp.content) self.assertEqual(resp['Content-Type'], XLSX_CT) def test_export(self): create_test_user(phone='13922200080', role_type='student') resp = self.client.get('/api/cms/users/export/') self.assertEqual(resp.status_code, 200) self.assertEqual(resp['Content-Type'], XLSX_CT) def test_import_users(self): f = make_xlsx( ['手机号', '姓名', '角色', '机构编码'], [ ['13922200091', '导入甲', '医生', 'CU-H001'], # ✅ ['13922200092', '导入乙', 'student', ''], # 机构编码空 → 失败 ['bad', '格式错', '学生', 'CU-H001'], # 手机号格式错 ['13922200093', '角色错', '不存在角色', 'CU-H001'], # 角色非法 ], ) resp = self.client.post('/api/cms/users/import/', {'file': f}, format='multipart') self.assertEqual(resp.status_code, 200, resp.content) body = resp.json() self.assertEqual(body['total'], 4) self.assertEqual(body['success'], 1) self.assertEqual(body['failed'], 3) self.assertTrue(User.objects.filter(phone='13922200091', role_type='doctor').exists()) def test_import_no_file(self): resp = self.client.post('/api/cms/users/import/', {}, format='multipart') self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json()['code'], 'CMS_IMPORT_FILE_REQUIRED')