1
0
forked from erp-dev/erp

feat: stateflow v2 (params required)

This commit is contained in:
2025-11-15 19:39:15 +08:00
parent 9ad404a365
commit 9c38e2ac09
24 changed files with 3670 additions and 547 deletions

View 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'])