""" 测试 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): """测试未开始时尝试回退(预期错误)""" # 验证进行中(废除了 not_started,初始状态也是 in_progress) self.assertEqual(services.get_overall_status(self.business_object), 'in_progress') # 尝试回退(应该失败) 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)