1
0
forked from erp-dev/erp
Files
erpnew/stateflow/tests/test_business_object_api.py

691 lines
29 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.
"""
测试 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'])
# 验证进度已重置current_state 返回第一个待执行的节点)
self.business_object.refresh_from_db()
current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.name, self.state1.name)
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'])
def test_get_state_logs_api_with_parameters(self):
"""测试获取状态流转记录列表(包含参数)"""
# 未开始时应返回空列表
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)
self.assertEqual(len(response.data['state_logs']), 0)
# 推进两次
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5', 'humidity': '60%'}},
format='json'
)
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}/state-logs/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['state_logs']), 2)
# 验证第一条记录包含参数摘要
log1 = response.data['state_logs'][0]
self.assertIn('parameters_summary', log1)
self.assertEqual(log1['parameters_summary']['temperature'], '25.5')
self.assertEqual(log1['parameters_summary']['humidity'], '60%')
self.assertEqual(log1['state_name'], '状态1')
# 验证第二条记录(无参数)
log2 = response.data['state_logs'][1]
self.assertIn('parameters_summary', log2)
self.assertEqual(len(log2['parameters_summary']), 0)
self.assertEqual(log2['state_name'], '状态2')
def test_get_state_logs_api_without_parameters(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/')
# 获取记录列表(不包含参数)
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/?include_parameters=false'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2)
# 验证不包含 parameters_summary 字段
log1 = response.data['state_logs'][0]
self.assertNotIn('parameters_summary', log1)
self.assertIn('state_name', log1)
self.assertIn('completed_at', log1)
self.assertIn('completed_by_username', log1)
def test_get_state_logs_api_exclude_cancelled(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}/step_back/')
# 默认包含已撤销记录
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/')
self.assertEqual(response.data['count'], 2)
# 验证第二条是已撤销的
self.assertTrue(response.data['state_logs'][1]['is_cancelled'])
# 排除已撤销记录
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/?include_cancelled=false'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(response.data['state_logs']), 1)
# 验证只有第一条记录(未撤销)
self.assertFalse(response.data['state_logs'][0]['is_cancelled'])
self.assertEqual(response.data['state_logs'][0]['state_name'], '状态1')
def test_get_state_logs_api_order(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}/state-logs/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 3)
# 验证按时间顺序排列
logs = response.data['state_logs']
self.assertEqual(logs[0]['state_name'], '状态1')
self.assertEqual(logs[1]['state_name'], '状态2')
self.assertEqual(logs[2]['state_name'], '状态3')
# 验证时间递增
from datetime import datetime
time1 = datetime.fromisoformat(logs[0]['completed_at'])
time2 = datetime.fromisoformat(logs[1]['completed_at'])
time3 = datetime.fromisoformat(logs[2]['completed_at'])
self.assertLessEqual(time1, time2)
self.assertLessEqual(time2, time3)
def test_advance_when_already_completed(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/')
# 验证已完成
self.assertEqual(services.get_overall_status(self.business_object), 'completed')
# 尝试再次推进(应该失败)
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('流程已完成', response.data['message'])
self.assertIn('无法继续推进', response.data['message'])
def test_step_back_when_not_started(self):
"""测试未开始时尝试回退(预期错误)"""
# 验证未开始
self.assertEqual(services.get_overall_status(self.business_object), 'not_started')
# 尝试回退(应该失败)
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('没有任何状态流转记录', response.data['message'])
def test_advance_process_without_nodes(self):
"""测试流程没有节点时尝试推进(预期错误)"""
# 创建一个没有节点的流程
empty_process = models.Process.objects.create(name='空流程')
empty_business_object = models.BusinessObject.objects.create(
name='空业务对象',
process=empty_process
)
# 尝试推进(应该失败)
response = self.client.post(
f'/api/v1/stateflow/business-objects/{empty_business_object.id}/advance/'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('流程没有任何节点', response.data['message'])
def test_get_process_nodes(self):
"""测试获取业务对象所属流程的所有状态节点 API"""
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/process-nodes/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('count', response.data)
self.assertIn('nodes', response.data)
# 验证节点数量
self.assertEqual(response.data['count'], 3)
self.assertEqual(len(response.data['nodes']), 3)
# 验证节点内容和顺序
nodes = response.data['nodes']
self.assertEqual(nodes[0]['state_id'], self.state1.id)
self.assertEqual(nodes[0]['state_name'], self.state1.name)
self.assertEqual(nodes[0]['order'], 0)
self.assertEqual(nodes[1]['state_id'], self.state2.id)
self.assertEqual(nodes[1]['state_name'], self.state2.name)
self.assertEqual(nodes[1]['order'], 1)
self.assertEqual(nodes[2]['state_id'], self.state3.id)
self.assertEqual(nodes[2]['state_name'], self.state3.name)
self.assertEqual(nodes[2]['order'], 2)
# 验证每个节点都有必要的字段
for node in nodes:
self.assertIn('id', node)
self.assertIn('state_id', node)
self.assertIn('state_name', node)
self.assertIn('order', node)