Files
medical_training/test/test_user_happy.py
T

381 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""用户域 4 条 happy-path 流程测试。"""
import time
from contextlib import ExitStack
from unittest.mock import patch
from django.core.cache import cache
from rest_framework.test import APIClient
from apps.user.throttling import (
SmsPhoneMinuteThrottle, SmsPhoneDayThrottle, SmsIpThrottle,
RegisterIpThrottle, ResetPhoneThrottle,
)
from .conftest import (
CacheTestCase,
USER_SEND_CODE_URL, USER_REGISTER_URL, USER_LOGIN_URL,
USER_LOGIN_CODE_URL, USER_CHANGE_PWD_URL, USER_RESET_PWD_URL, USER_ME_URL,
USER_LIST_URL, user_detail_url,
DEFAULT_INSTITUTION_CODE, DEFAULT_INSTITUTION_NAME,
inject_sms_code, create_test_user, get_auth_client, get_tokens,
create_teacher_student_relation,
)
def _bypass_all_auth_throttles(stack):
"""在 ExitStack 中注册所有认证相关限流 bypass。"""
for cls in (SmsPhoneMinuteThrottle, SmsPhoneDayThrottle, SmsIpThrottle,
RegisterIpThrottle, ResetPhoneThrottle):
stack.enter_context(patch.object(cls, 'allow_request', return_value=True))
class UserAuthHappyPathTest(CacheTestCase):
"""用户认证 happy-path 测试。"""
def setUp(self):
super().setUp()
self.client = APIClient()
# ── HP-1: 注册 → 密码登录 → /me ──────────────────────────────────────
def test_flow_register_login_me(self):
"""HP-1: U2 register(管理员代注册,默认密码) → U3 login(默认密码) → GET /me"""
phone = '13900000001'
default_password = f'Pass{phone}'
real_name = '张三'
with ExitStack() as stack:
_bypass_all_auth_throttles(stack)
# U2: register(管理员代注册,无需验证码,密码自动为 Pass+手机号)
resp = self.client.post(USER_REGISTER_URL, {
'phone': phone,
'real_name': real_name,
'institution_code': DEFAULT_INSTITUTION_CODE,
'institution_name': DEFAULT_INSTITUTION_NAME,
})
self.assertEqual(resp.status_code, 201, resp.content)
data = resp.json()
self.assertIn('tokens', data)
self.assertEqual(data['user']['phone'], phone)
self.assertEqual(data['user']['real_name'], real_name)
self.assertEqual(data['user']['institution_name'], DEFAULT_INSTITUTION_NAME)
self.assertEqual(data['user']['institution_code'], DEFAULT_INSTITUTION_CODE)
# U3: login (默认密码 Pass+手机号)
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': default_password,
})
self.assertEqual(resp.status_code, 200, resp.content)
tokens = resp.json()['tokens']
# GET /me
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {tokens["access"]}')
resp = self.client.get(USER_ME_URL)
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['phone'], phone)
self.assertEqual(resp.json()['real_name'], real_name)
# ── HP-2: 验证码登录 ─────────────────────────────────────────────────
def test_flow_code_login(self):
"""HP-2: 预创建用户 → U1 send-code(login) → U4 login-code → /me"""
phone = '13900000002'
user = create_test_user(phone=phone, password='TestPass1')
with ExitStack() as stack:
_bypass_all_auth_throttles(stack)
# U1: send-code (login)
resp = self.client.post(USER_SEND_CODE_URL, {
'phone': phone, 'scene': 'login',
})
self.assertEqual(resp.status_code, 200, resp.content)
code = cache.get(f'sms:login:{phone}')
self.assertIsNotNone(code)
# U4: login-code(需要 institution 字段)
resp = self.client.post(USER_LOGIN_CODE_URL, {
'phone': phone,
'code': str(code),
'institution_name': DEFAULT_INSTITUTION_NAME,
'institution_code': DEFAULT_INSTITUTION_CODE,
})
self.assertEqual(resp.status_code, 200, resp.content)
tokens = resp.json()['tokens']
self.assertIn('access', tokens)
self.assertIn('refresh', tokens)
self.assertFalse(resp.json()['is_new_user'])
# GET /me
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {tokens["access"]}')
resp = self.client.get(USER_ME_URL)
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['phone'], phone)
# ── HP-3: 重置密码 ──────────────────────────────────────────────────
def test_flow_reset_password(self):
"""HP-3: U1 send-code(reset) → U5 reset-password → U3 login(新密码)"""
phone = '13900000003'
old_pwd = 'OldPass1'
new_pwd = 'NewPass1'
create_test_user(phone=phone, password=old_pwd)
with ExitStack() as stack:
_bypass_all_auth_throttles(stack)
# U1: send-code (reset)
resp = self.client.post(USER_SEND_CODE_URL, {
'phone': phone, 'scene': 'reset',
})
self.assertEqual(resp.status_code, 200, resp.content)
code = cache.get(f'sms:reset:{phone}')
self.assertIsNotNone(code)
# U5: reset-password
resp = self.client.post(USER_RESET_PWD_URL, {
'phone': phone,
'code': str(code),
'new_password': new_pwd,
})
self.assertEqual(resp.status_code, 200, resp.content)
# 新密码登录成功
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': new_pwd,
})
self.assertEqual(resp.status_code, 200, resp.content)
# 旧密码登录失败
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': old_pwd,
})
self.assertIn(resp.status_code, (400, 401))
# ── HP-4: 修改密码 + 旧 token 失效 ──────────────────────────────────
def test_flow_change_password(self):
"""HP-4: login → U6 change-password → 旧 token 失效 → 新密码 login"""
phone = '13900000004'
old_pwd = 'OldPass1'
new_pwd = 'NewPass1'
user = create_test_user(phone=phone, password=old_pwd)
# U3: login
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': old_pwd,
})
self.assertEqual(resp.status_code, 200, resp.content)
old_access = resp.json()['tokens']['access']
# U6: change-password
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {old_access}')
resp = self.client.post(USER_CHANGE_PWD_URL, {
'old_password': old_pwd,
'new_password': new_pwd,
})
self.assertEqual(resp.status_code, 200, resp.content)
# 等待 1 秒:invalidate_user_tokens 写入 time()+1
time.sleep(1)
# 旧 token 应被拒绝
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {old_access}')
resp = self.client.get(USER_ME_URL)
self.assertEqual(resp.status_code, 401, f'旧 token 应失效: {resp.content}')
# 新密码登录
self.client.credentials() # 清除旧 auth
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': new_pwd,
})
self.assertEqual(resp.status_code, 200, resp.content)
new_access = resp.json()['tokens']['access']
# 新 token 正常
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {new_access}')
resp = self.client.get(USER_ME_URL)
self.assertEqual(resp.status_code, 200, resp.content)
class UserListDetailHappyPathTest(CacheTestCase):
"""U9 用户列表 + U10 用户详情 happy-path 测试。"""
# ── HP-5: 管理员获取全部用户列表 ─────────────────────────────────────
def test_admin_list_all_users(self):
"""HP-5: admin GET /users/ → 200,可见全部用户"""
admin = create_test_user(
phone='13900100001', password='Admin123',
real_name='管理员', role_type='super_admin',
)
stu1 = create_test_user(
phone='13900100002', password='Stu12345',
real_name='学生A', role_type='student',
)
stu2 = create_test_user(
phone='13900100003', password='Stu12345',
real_name='学生B', role_type='student',
)
client = get_auth_client(admin)
resp = client.get(USER_LIST_URL)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
# DRF 分页:results 列表
results = data.get('results', data)
result_ids = [u['id'] for u in results]
self.assertIn(stu1.id, result_ids)
self.assertIn(stu2.id, result_ids)
self.assertIn(admin.id, result_ids)
# ── HP-6: 教师仅看到自己名下学生 ─────────────────────────────────────
def test_teacher_list_own_students_only(self):
"""HP-6: teacher GET /users/ → 200,仅包含名下活跃学生"""
teacher = create_test_user(
phone='13900100010', password='Teacher1',
real_name='王老师', role_type='teacher',
)
stu_own = create_test_user(
phone='13900100011', password='Stu12345',
real_name='我的学生', role_type='student',
)
stu_other = create_test_user(
phone='13900100012', password='Stu12345',
real_name='其他学生', role_type='student',
)
create_teacher_student_relation(teacher, stu_own, status=1)
# stu_other 无关系
client = get_auth_client(teacher)
resp = client.get(USER_LIST_URL)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
results = data.get('results', data)
result_ids = [u['id'] for u in results]
self.assertIn(stu_own.id, result_ids)
self.assertNotIn(stu_other.id, result_ids)
# 教师自己也不应出现在列表(queryset 过滤 role_type='student'
self.assertNotIn(teacher.id, result_ids)
# ── HP-7: 教师不可见已结束关系的学生 ─────────────────────────────────
def test_teacher_list_excludes_ended_relation(self):
"""HP-7: 已结束(status=0)的师生关系学生不出现在列表"""
teacher = create_test_user(
phone='13900100020', password='Teacher1',
real_name='李老师', role_type='teacher',
)
stu_active = create_test_user(
phone='13900100021', password='Stu12345',
real_name='活跃学生', role_type='student',
)
stu_ended = create_test_user(
phone='13900100022', password='Stu12345',
real_name='已毕业学生', role_type='student',
)
create_teacher_student_relation(teacher, stu_active, status=1)
create_teacher_student_relation(teacher, stu_ended, status=0)
client = get_auth_client(teacher)
resp = client.get(USER_LIST_URL)
self.assertEqual(resp.status_code, 200, resp.content)
results = resp.json().get('results', resp.json())
result_ids = [u['id'] for u in results]
self.assertIn(stu_active.id, result_ids)
self.assertNotIn(stu_ended.id, result_ids)
# ── HP-8: 管理员查看任意用户详情 ─────────────────────────────────────
def test_admin_retrieve_any_user(self):
"""HP-8: admin GET /users/{id}/ → 200,可查看任意用户"""
admin = create_test_user(
phone='13900100030', password='Admin123',
real_name='管理员', role_type='super_admin',
)
student = create_test_user(
phone='13900100031', password='Stu12345',
real_name='某学生', role_type='student',
)
client = get_auth_client(admin)
resp = client.get(user_detail_url(student.id))
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['id'], student.id)
self.assertEqual(resp.json()['real_name'], '某学生')
# ── HP-9: 用户查看自己的详情 ─────────────────────────────────────────
def test_self_retrieve(self):
"""HP-9: student GET /users/{self.id}/ → 200,可查看自己"""
student = create_test_user(
phone='13900100040', password='Stu12345',
real_name='自查学生', role_type='student',
)
client = get_auth_client(student)
resp = client.get(user_detail_url(student.id))
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['id'], student.id)
# ── HP-10: 教师查看名下学生详情 ──────────────────────────────────────
def test_teacher_retrieve_own_student(self):
"""HP-10: teacher GET /users/{student.id}/ → 200,可查看名下学生"""
teacher = create_test_user(
phone='13900100050', password='Teacher1',
real_name='赵老师', role_type='teacher',
)
student = create_test_user(
phone='13900100051', password='Stu12345',
real_name='赵的学生', role_type='student',
)
create_teacher_student_relation(teacher, student, status=1)
client = get_auth_client(teacher)
resp = client.get(user_detail_url(student.id))
self.assertEqual(resp.status_code, 200, resp.content)
self.assertEqual(resp.json()['id'], student.id)
# ── HP-11: 管理员列表支持过滤和搜索 ──────────────────────────────────
def test_admin_list_filter_and_search(self):
"""HP-11: admin GET /users/?role_type=student&search=张 → 过滤生效"""
admin = create_test_user(
phone='13900100060', password='Admin123',
real_name='管理员', role_type='super_admin',
)
stu_zhang = create_test_user(
phone='13900100061', password='Stu12345',
real_name='张同学', role_type='student',
)
stu_li = create_test_user(
phone='13900100062', password='Stu12345',
real_name='李同学', role_type='student',
)
teacher = create_test_user(
phone='13900100063', password='Teacher1',
real_name='张老师', role_type='teacher',
)
client = get_auth_client(admin)
# 按 role_type 过滤 + search
resp = client.get(USER_LIST_URL, {'role_type': 'student', 'search': ''})
self.assertEqual(resp.status_code, 200, resp.content)
results = resp.json().get('results', resp.json())
result_ids = [u['id'] for u in results]
self.assertIn(stu_zhang.id, result_ids)
self.assertNotIn(stu_li.id, result_ids)
# 张老师 role_type=teacher,被 role_type=student 过滤掉
self.assertNotIn(teacher.id, result_ids)