""" 测试 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'], 'in_progress') 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_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'])