Files
medical_training/test/test_user_happy.py
T

384 lines
15 KiB
Python
Raw Normal View History

2026-05-29 15:58:00 +08:00
"""用户域 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,
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: U1 send-code(register) → U2 register → U3 login → GET /me"""
phone = '13900000001'
password = 'Abc12345'
real_name = '张三'
with ExitStack() as stack:
_bypass_all_auth_throttles(stack)
# U1: send-code (register)
resp = self.client.post(USER_SEND_CODE_URL, {
'phone': phone, 'scene': 'register',
})
self.assertEqual(resp.status_code, 200, resp.content)
# 从 cache 读验证码
code = cache.get(f'sms:register:{phone}')
self.assertIsNotNone(code, '验证码未写入缓存')
# U2: register
resp = self.client.post(USER_REGISTER_URL, {
'phone': phone,
'code': str(code),
'password': password,
'real_name': real_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)
# U3: login (password) — 限流 bypass 已退出,login 无限流
resp = self.client.post(USER_LOGIN_URL, {
'phone': phone, 'password': 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
resp = self.client.post(USER_LOGIN_CODE_URL, {
'phone': phone, 'code': str(code),
})
self.assertEqual(resp.status_code, 200, resp.content)
tokens = resp.json()['tokens']
self.assertIn('access', tokens)
self.assertIn('refresh', 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)
# ── 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)