forked from erp-dev/erp
feat: stateflow v2 (params required)
This commit is contained in:
471
stateflow/tests/test_business_object_api.py
Normal file
471
stateflow/tests/test_business_object_api.py
Normal file
@@ -0,0 +1,471 @@
|
||||
"""
|
||||
测试 BusinessObject 的完整 API 端点
|
||||
包括 reset, timeline, add_parameters_to_log, get_log_parameters 等
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
from stateflow import models, services
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BusinessObjectAPITestCase(TestCase):
|
||||
"""测试 BusinessObject 的所有 API 端点"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.user = User.objects.create_user(username='testuser', password='testpass')
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
# 创建状态
|
||||
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
|
||||
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
|
||||
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
|
||||
|
||||
# 为状态添加参数
|
||||
self.param1 = models.StateParameter.objects.create(
|
||||
key='temperature',
|
||||
value='',
|
||||
is_required=True,
|
||||
description='温度'
|
||||
)
|
||||
self.param2 = models.StateParameter.objects.create(
|
||||
key='humidity',
|
||||
value='',
|
||||
is_required=False,
|
||||
description='湿度'
|
||||
)
|
||||
self.state1.parameters.add(self.param1, self.param2)
|
||||
|
||||
# 创建流程
|
||||
self.process = models.Process.objects.create(name='测试流程')
|
||||
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
|
||||
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
|
||||
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
|
||||
|
||||
# 创建业务对象
|
||||
self.business_object = models.BusinessObject.objects.create(
|
||||
name='测试业务对象',
|
||||
process=self.process
|
||||
)
|
||||
|
||||
def test_reset_api(self):
|
||||
"""测试重置进度 API"""
|
||||
# 先推进两次
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
|
||||
|
||||
# 验证已有进度
|
||||
self.business_object.refresh_from_db()
|
||||
self.assertIsNotNone(services.get_business_object_current_state(self.business_object))
|
||||
self.assertEqual(self.business_object.state_logs.count(), 2)
|
||||
|
||||
# 重置进度
|
||||
response = self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/reset/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['success'])
|
||||
self.assertIn('重置', response.data['message'])
|
||||
|
||||
# 验证进度已重置
|
||||
self.business_object.refresh_from_db()
|
||||
self.assertIsNone(services.get_business_object_current_state(self.business_object))
|
||||
self.assertEqual(self.business_object.state_logs.filter(is_cancelled=False).count(), 0)
|
||||
|
||||
def test_reset_api_not_started(self):
|
||||
"""测试重置未开始的业务对象"""
|
||||
# 未开始也可以重置(不会报错)
|
||||
response = self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/reset/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['success'])
|
||||
|
||||
def test_timeline_api(self):
|
||||
"""测试状态时间线 API"""
|
||||
# 未开始时的时间线
|
||||
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
# 验证所有状态都是待执行
|
||||
for item in response.data:
|
||||
self.assertEqual(item['status'], 'not_started')
|
||||
self.assertIsNone(item['completed_at'])
|
||||
self.assertIsNone(item['completed_by'])
|
||||
self.assertFalse(item['is_cancelled'])
|
||||
|
||||
# 推进第一个状态
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
# 再次获取时间线
|
||||
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
# 第一个状态应该是已完成
|
||||
self.assertEqual(response.data[0]['status'], 'completed')
|
||||
self.assertIsNotNone(response.data[0]['completed_at'])
|
||||
self.assertEqual(response.data[0]['completed_by'], 'testuser')
|
||||
self.assertFalse(response.data[0]['is_cancelled'])
|
||||
|
||||
# 其他状态仍是待执行
|
||||
self.assertEqual(response.data[1]['status'], 'not_started')
|
||||
self.assertEqual(response.data[2]['status'], 'not_started')
|
||||
|
||||
def test_timeline_api_with_step_back(self):
|
||||
"""测试时间线 API 包含回退后的已撤销状态"""
|
||||
# 推进两次
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
|
||||
|
||||
# 回退一次
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
|
||||
|
||||
# 获取时间线
|
||||
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 第一个状态:已完成且未撤销
|
||||
self.assertEqual(response.data[0]['status'], 'completed')
|
||||
self.assertFalse(response.data[0]['is_cancelled'])
|
||||
|
||||
# 第二个状态:已撤销
|
||||
self.assertEqual(response.data[1]['status'], 'cancelled')
|
||||
self.assertTrue(response.data[1]['is_cancelled'])
|
||||
self.assertIsNotNone(response.data[1]['cancelled_at'])
|
||||
|
||||
# 第三个状态:待执行
|
||||
self.assertEqual(response.data[2]['status'], 'not_started')
|
||||
|
||||
def test_add_parameters_to_log_api(self):
|
||||
"""测试为状态流转记录补充参数 API"""
|
||||
# 先推进一次
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 补充参数
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{
|
||||
'parameters': {
|
||||
'humidity': '60%',
|
||||
'operator': '张三'
|
||||
},
|
||||
'remark': '补充测量数据'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['success'])
|
||||
self.assertIn('parameter_record', response.data)
|
||||
|
||||
# 验证参数记录
|
||||
param_record = response.data['parameter_record']
|
||||
self.assertEqual(param_record['parameters']['humidity'], '60%')
|
||||
self.assertEqual(param_record['parameters']['operator'], '张三')
|
||||
self.assertEqual(param_record['remark'], '补充测量数据')
|
||||
|
||||
def test_add_parameters_to_log_api_empty_parameters(self):
|
||||
"""测试补充空参数应该失败"""
|
||||
# 先推进一次
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 尝试补充空参数
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{
|
||||
'parameters': {},
|
||||
'remark': '空参数测试'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(response.data['success'])
|
||||
self.assertIn('不能为空', response.data['message'])
|
||||
|
||||
def test_add_parameters_to_log_api_nonexistent_log(self):
|
||||
"""测试为不存在的记录补充参数应该失败"""
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/99999/add-parameters/',
|
||||
{
|
||||
'parameters': {'test': 'value'},
|
||||
'remark': '测试'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertFalse(response.data['success'])
|
||||
self.assertIn('不存在', response.data['message'])
|
||||
|
||||
def test_get_log_parameters_api_all_records(self):
|
||||
"""测试获取状态流转记录的所有参数记录 API"""
|
||||
# 推进并提供初始参数
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5', 'humidity': '60%'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 补充参数两次
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{
|
||||
'parameters': {'temperature': '26.0'},
|
||||
'remark': '第一次重测'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{
|
||||
'parameters': {'temperature': '26.5'},
|
||||
'remark': '第二次重测'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
# 获取所有参数记录
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['state_log_id'], state_log_id)
|
||||
self.assertFalse(response.data['is_cancelled'])
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
# 验证汇总数据
|
||||
summary = response.data['summary']
|
||||
self.assertEqual(summary['temperature'], '26.5') # 最新值
|
||||
self.assertEqual(summary['humidity'], '60%')
|
||||
|
||||
# 验证记录列表
|
||||
self.assertEqual(len(response.data['records']), 3)
|
||||
|
||||
def test_get_log_parameters_api_by_key(self):
|
||||
"""测试获取指定参数的历史记录 API"""
|
||||
# 推进并提供初始参数
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 补充参数
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{
|
||||
'parameters': {'temperature': '26.0', 'humidity': '65%'},
|
||||
'remark': '重测'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
# 获取 temperature 的历史
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['key'], 'temperature')
|
||||
self.assertEqual(len(response.data['history']), 2)
|
||||
self.assertEqual(response.data['history'][0]['value'], '25.5')
|
||||
self.assertEqual(response.data['history'][1]['value'], '26.0')
|
||||
self.assertEqual(response.data['history'][1]['remark'], '重测')
|
||||
|
||||
def test_get_log_parameters_api_cancelled_state_excluded(self):
|
||||
"""测试已撤销状态的参数默认不可见"""
|
||||
# 推进
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 回退(撤销状态)
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
|
||||
|
||||
# 默认获取参数(应该返回空)
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['is_cancelled'])
|
||||
self.assertEqual(len(response.data['summary']), 0) # 汇总应该为空
|
||||
self.assertEqual(len(response.data['records']), 0) # 记录列表应该为空
|
||||
|
||||
def test_get_log_parameters_api_cancelled_state_with_include_flag(self):
|
||||
"""测试 include_cancelled=true 可以查看已撤销状态的参数"""
|
||||
# 推进
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5', 'humidity': '60%'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 回退(撤销状态)
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
|
||||
|
||||
# 使用 include_cancelled=true 获取参数
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?include_cancelled=true'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['is_cancelled'])
|
||||
self.assertEqual(len(response.data['summary']), 2) # 应该能看到参数
|
||||
self.assertEqual(response.data['summary']['temperature'], '25.5')
|
||||
self.assertEqual(response.data['summary']['humidity'], '60%')
|
||||
|
||||
def test_get_log_parameters_api_key_history_cancelled_excluded(self):
|
||||
"""测试已撤销状态的参数历史默认不可见"""
|
||||
# 推进
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
|
||||
state_log_id = response.data['state_log']['id']
|
||||
|
||||
# 补充参数
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
|
||||
{'parameters': {'temperature': '26.0'}, 'remark': '重测'},
|
||||
format='json'
|
||||
)
|
||||
|
||||
# 回退
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
|
||||
|
||||
# 获取参数历史(默认应该为空)
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data['history']), 0)
|
||||
|
||||
# 使用 include_cancelled=true 查看
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature&include_cancelled=true'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data['history']), 2)
|
||||
|
||||
def test_get_log_parameters_api_nonexistent_log(self):
|
||||
"""测试获取不存在的记录的参数应该返回 404"""
|
||||
response = self.client.get(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/99999/parameters/'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertFalse(response.data['success'])
|
||||
|
||||
def test_advance_api_with_parameters(self):
|
||||
"""测试推进 API 携带参数"""
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{
|
||||
'parameters': {
|
||||
'temperature': '25.5',
|
||||
'humidity': '60%',
|
||||
'operator': '张三'
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data['success'])
|
||||
self.assertIn('state_log', response.data)
|
||||
|
||||
# 验证参数摘要(API 返回的是 parameters_summary,不是 parameter_records)
|
||||
state_log = response.data['state_log']
|
||||
self.assertIn('parameters_summary', state_log)
|
||||
|
||||
params = state_log['parameters_summary']
|
||||
self.assertEqual(params['temperature'], '25.5')
|
||||
self.assertEqual(params['humidity'], '60%')
|
||||
self.assertEqual(params['operator'], '张三')
|
||||
|
||||
def test_advance_api_missing_required_parameter(self):
|
||||
"""测试推进 API 缺少必填参数应该失败"""
|
||||
response = self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{
|
||||
'parameters': {
|
||||
'humidity': '60%' # 只提供可选参数,缺少必填的 temperature
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(response.data['success'])
|
||||
self.assertIn('缺失必填参数', response.data['message'])
|
||||
self.assertIn('temperature', response.data['message'])
|
||||
|
||||
def test_timeline_completed_all_states(self):
|
||||
"""测试完成所有状态后的时间线"""
|
||||
# 完成所有状态
|
||||
self.client.post(
|
||||
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
|
||||
{'parameters': {'temperature': '25.5'}},
|
||||
format='json'
|
||||
)
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
|
||||
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
|
||||
|
||||
# 获取时间线
|
||||
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 所有状态都应该是已完成
|
||||
for item in response.data:
|
||||
self.assertEqual(item['status'], 'completed')
|
||||
self.assertIsNotNone(item['completed_at'])
|
||||
self.assertIsNotNone(item['completed_by'])
|
||||
self.assertFalse(item['is_cancelled'])
|
||||
Reference in New Issue
Block a user