class TestStatusApiPatch(TestCase): """Test CycleTask's statuses change via PATCH operation.""" ASSIGNED = all_models.CycleTaskGroupObjectTask.ASSIGNED IN_PROGRESS = all_models.CycleTaskGroupObjectTask.IN_PROGRESS FINISHED = all_models.CycleTaskGroupObjectTask.FINISHED DEPRECATED = all_models.CycleTaskGroupObjectTask.DEPRECATED VERIFIED = all_models.CycleTaskGroupObjectTask.VERIFIED DECLINED = all_models.CycleTaskGroupObjectTask.DECLINED def _create_cycle_structure(self): """Create cycle structure. It will create workflow, cycle, group and 3 tasks in that group. Retruns tuple: workflow, cycle, group and list of tasks. """ wf_slug = "WF-SLUG-{}".format(factories.random_str( length=6, chars=string.ascii_letters)) with factories.single_commit(): workflow = wf_factories.WorkflowFactory(slug=wf_slug) task_group = wf_factories.TaskGroupFactory(workflow=workflow) for ind in xrange(3): wf_factories.TaskGroupTaskFactory( task_group=task_group, title='task{}'.format(ind), start_date=datetime.datetime.now(), end_date=datetime.datetime.now() + datetime.timedelta(7) ) data = workflow_api.get_cycle_post_dict(workflow) self.api.post(all_models.Cycle, data) workflow = all_models.Workflow.query.filter_by(slug=wf_slug).one() cycle = all_models.Cycle.query.filter_by(workflow_id=workflow.id).one() group = all_models.CycleTaskGroup.query.filter_by(cycle_id=cycle.id).one() tasks = all_models.CycleTaskGroupObjectTask.query.filter_by( cycle_id=cycle.id).all() return workflow, cycle, group, tasks def setUp(self): super(TestStatusApiPatch, self).setUp() self.api = Api() with factories.single_commit(): self.assignee = factories.PersonFactory(email="*****@*****.**") ( self.workflow, self.cycle, self.group, self.tasks, ) = self._create_cycle_structure() self.group_id = self.group.id # Emulate that current user is assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfa_or_assignee = ( MagicMock(return_value=True)) def _update_ct_via_patch(self, new_states): """Update CycleTasks' state via PATCH. Args: new_states: List of states to which CTs should be moved via PATCH. Returns: JSON response which was returned by back-end. """ data = [{"state": state, "id": self.tasks[ind].id} for ind, state in enumerate(new_states)] resp = self.api.patch(all_models.CycleTaskGroupObjectTask, data) return resp.json def _get_exp_response(self, exp_res): """Format expected response from CycleTasks' test data indexes. Args: exp_res: Expected results dict: {test_ctask_index: response_status} Returns: [{'id': updated_task_id, 'status': 'success|skipped|error'}, ...] """ return [{'id': self.tasks[ind].id, 'status': status} for ind, status in exp_res.iteritems()] @ddt.data( ( # New statuses which we try to set during the test [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS], # Expected results: {test_ctask_index: response_status} {0: 'updated', 1: 'updated', 2: 'updated'}, # Expected states after update [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS] ), ( [DEPRECATED, DEPRECATED, DEPRECATED], {0: 'updated', 1: 'updated', 2: 'updated'}, [DEPRECATED, DEPRECATED, DEPRECATED] ), ( [FINISHED, FINISHED, FINISHED], {0: 'skipped', 1: 'skipped', 2: 'skipped'}, [ASSIGNED, ASSIGNED, ASSIGNED] ) ) @ddt.unpack def test_ct_status_bulk_update_ok(self, new_states, exp_res, exp_states): """Check CycleTasks' state update with valid data via PATCH.""" self.assertItemsEqual(self._get_exp_response(exp_res), self._update_ct_via_patch(new_states)) self.assertItemsEqual( exp_states, [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_json_error(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid data via PATCH.""" exp_resp = { "message": "Request's JSON contains multiple statuses for CycleTasks", "code": 400 } self.assertEqual(exp_resp, self._update_ct_via_patch([self.IN_PROGRESS, self.FINISHED, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_without_permissions(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update without permissions via PATCH.""" # Emulate that current user is not assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfa_or_assignee = ( MagicMock(return_value=False)) self.assertItemsEqual( self._get_exp_response({0: 'skipped', 1: 'skipped', 2: 'skipped'}), self._update_ct_via_patch([self.IN_PROGRESS, self.IN_PROGRESS, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_invalid_state(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid statuses via PATCH.""" exp_resp = { "message": "Request's JSON contains invalid statuses for CycleTasks", "code": 400 } self.assertItemsEqual(exp_resp, self._update_ct_via_patch(['InVaLiD', 'InVaLiD'])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def refresh_set_up_instances(self): """Update set up instances after request operation.""" self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id ).all() self.group = all_models.CycleTaskGroup.query.get(self.group_id) self.cycle = self.group.cycle self.workflow = self.group.cycle.workflow def assert_notifications_for_object(self, obj, *expected_notification_list): """Assert object notifications are equal to expected notification list.""" active_notifications = all_models.Notification.query.filter( all_models.Notification.object_id == obj.id, all_models.Notification.object_type == obj.type, sa.or_(all_models.Notification.sent_at.is_(None), all_models.Notification.repeating == sa.true()) ).all() self.assertEqual( sorted(expected_notification_list), sorted([n.notification_type.name for n in active_notifications]) ) def assert_latest_revision_status(self, *obj_status_chain): """Assert last status for object and status chain.""" objs_status_dict = {(o.type, o.id): s for o, s in obj_status_chain} revisions = [] for o_type, o_id in objs_status_dict: revisions.append(all_models.Revision.query.filter( all_models.Revision.resource_id == o_id, all_models.Revision.resource_type == o_type )) revisions_query = revisions[0].union( *revisions[1:] ).order_by( all_models.Revision.id ) revisions_dict = collections.defaultdict(list) for revision in revisions_query: key = (revision.resource_type, revision.resource_id) revisions_dict[key].append(revision.content) for key, status in objs_status_dict.iteritems(): self.assertIn(key, revisions_dict) content = revisions_dict[key][-1] self.assertIn("status", content) self.assertEqual(status, content["status"]) def assert_searchable_by_status(self, *obj_status_chain): """Assert expected status in full text search.""" objs_status_dict = {(o.type, o.id): s for o, s in obj_status_chain} full_text_properties = [] for o_type, o_id in objs_status_dict: full_text_properties.append( mysql.MysqlRecordProperty.query.filter( mysql.MysqlRecordProperty.key == o_id, mysql.MysqlRecordProperty.type == o_type, mysql.MysqlRecordProperty.property == "task status", ) ) query = full_text_properties[0].union(*full_text_properties[1:]) full_text_dict = {(f.type, f.key): f.content for f in query} for key, status in objs_status_dict.iteritems(): self.assertIn(key, full_text_dict) self.assertEqual(status, full_text_dict[key]) def assert_status_over_bulk_update(self, statuses, assert_statuses): """Assertion cycle_task statuses over bulk update.""" self._update_ct_via_patch(statuses) self.refresh_set_up_instances() self.assertItemsEqual(assert_statuses, [obj.status for obj in self.tasks]) obj_status_chain = [ (t, assert_statuses[idx]) for idx, t in enumerate(self.tasks) ] self.assert_latest_revision_status(*obj_status_chain) self.assert_searchable_by_status(*obj_status_chain) def test_propagation_status_full(self): """Task status propagation for required verification workflow.""" # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 3, [self.IN_PROGRESS] * 3) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update( [self.FINISHED], [self.FINISHED, self.IN_PROGRESS, self.IN_PROGRESS]) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 3, [self.FINISHED] * 3) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u'manual_cycle_created') # all tasks moved to verified self.assert_status_over_bulk_update([self.VERIFIED] * 3, [self.VERIFIED] * 3) self.assertEqual(self.VERIFIED, self.group.status) self.assertEqual(self.VERIFIED, self.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') def test_propagation_status_short(self): """Task status propagation for not required verification workflow.""" all_models.Cycle.query.filter( all_models.Cycle.id == self.cycle.id ).update({ all_models.Cycle.is_verification_needed: False }) db.session.commit() self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id ).all() # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 3, [self.IN_PROGRESS] * 3) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update( [self.FINISHED], [self.FINISHED, self.IN_PROGRESS, self.IN_PROGRESS]) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assert_notifications_for_object(self.tasks[0]) for task in self.tasks[1:]: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u'manual_cycle_created') # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 3, [self.FINISHED] * 3) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') def test_deprecated_final(self): """Test task status propagation for deprecated workflow.""" self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.DEPRECATED] * 3, [self.DEPRECATED] * 3) self.assertEqual(self.DEPRECATED, self.group.status) self.assertEqual(self.DEPRECATED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assert_status_over_bulk_update( [self.ASSIGNED, self.VERIFIED, self.FINISHED], [self.DEPRECATED] * 3) self.assertEqual(self.DEPRECATED, self.group.status) self.assertEqual(self.DEPRECATED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) @ddt.data({"to_state": FINISHED, "success": True}, {"to_state": IN_PROGRESS, "success": False}, {"to_state": ASSIGNED, "success": False}, {"to_state": VERIFIED, "success": False}) @ddt.unpack def test_move_from_declined(self, to_state, success): """Test status propagation over update declined tasks to {to_state}.""" self.assertEqual([self.ASSIGNED] * len(self.tasks), [t.status for t in self.tasks]) start_state = self.DECLINED start_statuses = [start_state] * len(self.tasks) with factories.single_commit(): for idx, status in enumerate(start_statuses): self.tasks[idx].status = status self.group.status = self.IN_PROGRESS self.cycle.status = self.IN_PROGRESS self.workflow.status = all_models.Workflow.ACTIVE self.assertEqual(start_statuses, [t.status for t in self.tasks]) # all tasks in progress state to_states = [to_state] * len(self.tasks) self._update_ct_via_patch(to_states) self.refresh_set_up_instances() if success: task_state = group_state = cycle_state = to_state else: task_state = start_state group_state = cycle_state = self.IN_PROGRESS self.assertEqual([task_state] * len(self.tasks), [t.status for t in self.tasks]) self.assertEqual(group_state, self.group.status) self.assertEqual(cycle_state, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) def test_update_tasks_from_2_cycles(self): """Test bulk update few cycles at the same time.""" with factories.single_commit(): _, _, group, _ = self._create_cycle_structure() group_id = group.id all_models.Cycle.query.update({ all_models.Cycle.is_verification_needed: False }) db.session.commit() self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id ).all() # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 6, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 6, [self.IN_PROGRESS] * 6) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(self.IN_PROGRESS, group.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.ACTIVE, group.cycle.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update( [self.FINISHED], [self.FINISHED] + [self.IN_PROGRESS] * 5) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(self.IN_PROGRESS, group.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.ACTIVE, group.cycle.workflow.status) self.assert_notifications_for_object(self.tasks[0]) for task in self.tasks[1:]: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.assert_notifications_for_object(self.group.cycle, u'manual_cycle_created') self.assert_notifications_for_object(group.cycle, u'manual_cycle_created') # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 6, [self.FINISHED] * 6) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(self.FINISHED, group.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.INACTIVE, group.cycle.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') self.assert_notifications_for_object(group.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created')
class TestStatusApiPatch(TestCase): """Test CycleTask's statuses change via PATCH operation.""" ASSIGNED = all_models.CycleTaskGroupObjectTask.ASSIGNED IN_PROGRESS = all_models.CycleTaskGroupObjectTask.IN_PROGRESS FINISHED = all_models.CycleTaskGroupObjectTask.FINISHED DEPRECATED = all_models.CycleTaskGroupObjectTask.DEPRECATED VERIFIED = all_models.CycleTaskGroupObjectTask.VERIFIED DECLINED = all_models.CycleTaskGroupObjectTask.DECLINED def _create_cycle_structure(self): """Create cycle structure. It will create workflow, cycle, group and 3 tasks in that group. Retruns tuple: workflow, cycle, group and list of tasks. """ wf_slug = "WF-SLUG-{}".format( factories.random_str(length=6, chars=string.ascii_letters)) with factories.single_commit(): workflow = wf_factories.WorkflowFactory(slug=wf_slug) task_group = wf_factories.TaskGroupFactory(workflow=workflow) for ind in xrange(3): wf_factories.TaskGroupTaskFactory( task_group=task_group, title='task{}'.format(ind), start_date=datetime.datetime.now(), end_date=datetime.datetime.now() + datetime.timedelta(7)) data = workflow_api.get_cycle_post_dict(workflow) self.api.post(all_models.Cycle, data) workflow = all_models.Workflow.query.filter_by(slug=wf_slug).one() cycle = all_models.Cycle.query.filter_by(workflow_id=workflow.id).one() group = all_models.CycleTaskGroup.query.filter_by( cycle_id=cycle.id).one() tasks = all_models.CycleTaskGroupObjectTask.query.filter_by( cycle_id=cycle.id).all() return workflow, cycle, group, tasks def setUp(self): super(TestStatusApiPatch, self).setUp() self.api = Api() with factories.single_commit(): self.assignee = factories.PersonFactory( email="*****@*****.**") ( self.workflow, self.cycle, self.group, self.tasks, ) = self._create_cycle_structure() self.group_id = self.group.id # Emulate that current user is assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfa_or_assignee = ( MagicMock(return_value=True)) def _update_ct_via_patch(self, new_states): """Update CycleTasks' state via PATCH. Args: new_states: List of states to which CTs should be moved via PATCH. Returns: JSON response which was returned by back-end. """ data = [{ "state": state, "id": self.tasks[ind].id } for ind, state in enumerate(new_states)] resp = self.api.patch(all_models.CycleTaskGroupObjectTask, data) return resp.json def _get_exp_response(self, exp_res): """Format expected response from CycleTasks' test data indexes. Args: exp_res: Expected results dict: {test_ctask_index: response_status} Returns: [{'id': updated_task_id, 'status': 'success|skipped|error'}, ...] """ return [{ 'id': self.tasks[ind].id, 'status': status } for ind, status in exp_res.iteritems()] @ddt.data( ( # New statuses which we try to set during the test [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS], # Expected results: {test_ctask_index: response_status} { 0: 'updated', 1: 'updated', 2: 'updated' }, # Expected states after update [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS]), ([DEPRECATED, DEPRECATED, DEPRECATED], { 0: 'updated', 1: 'updated', 2: 'updated' }, [DEPRECATED, DEPRECATED, DEPRECATED]), ([FINISHED, FINISHED, FINISHED], { 0: 'skipped', 1: 'skipped', 2: 'skipped' }, [ASSIGNED, ASSIGNED, ASSIGNED])) @ddt.unpack def test_ct_status_bulk_update_ok(self, new_states, exp_res, exp_states): """Check CycleTasks' state update with valid data via PATCH.""" self.assertItemsEqual(self._get_exp_response(exp_res), self._update_ct_via_patch(new_states)) self.assertItemsEqual( exp_states, [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_json_error(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid data via PATCH.""" exp_resp = { "message": "Request's JSON contains multiple statuses for CycleTasks", "code": 400 } self.assertEqual( exp_resp, self._update_ct_via_patch( [self.IN_PROGRESS, self.FINISHED, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_without_permissions(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update without permissions via PATCH.""" # Emulate that current user is not assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfa_or_assignee = ( MagicMock(return_value=False)) self.assertItemsEqual( self._get_exp_response({ 0: 'skipped', 1: 'skipped', 2: 'skipped' }), self._update_ct_via_patch( [self.IN_PROGRESS, self.IN_PROGRESS, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_invalid_state(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid statuses via PATCH.""" exp_resp = { "message": "Request's JSON contains invalid statuses for CycleTasks", "code": 400 } self.assertItemsEqual( exp_resp, self._update_ct_via_patch(['InVaLiD', 'InVaLiD'])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def refresh_set_up_instances(self): """Update set up instances after request operation.""" self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id).all() self.group = all_models.CycleTaskGroup.query.get(self.group_id) self.cycle = self.group.cycle self.workflow = self.group.cycle.workflow def assert_notifications_for_object(self, obj, *expected_notification_list): """Assert object notifications are equal to expected notification list.""" active_notifications = all_models.Notification.query.filter( all_models.Notification.object_id == obj.id, all_models.Notification.object_type == obj.type, sa.or_(all_models.Notification.sent_at.is_(None), all_models.Notification.repeating == sa.true())).all() self.assertEqual( sorted(expected_notification_list), sorted([n.notification_type.name for n in active_notifications])) def assert_latest_revision_status(self, *obj_status_chain): """Assert last status for object and status chain.""" objs_status_dict = {(o.type, o.id): s for o, s in obj_status_chain} revisions = [] for o_type, o_id in objs_status_dict: revisions.append( all_models.Revision.query.filter( all_models.Revision.resource_id == o_id, all_models.Revision.resource_type == o_type)) revisions_query = revisions[0].union(*revisions[1:]).order_by( all_models.Revision.id) revisions_dict = collections.defaultdict(list) for revision in revisions_query: key = (revision.resource_type, revision.resource_id) revisions_dict[key].append(revision.content) for key, status in objs_status_dict.iteritems(): self.assertIn(key, revisions_dict) content = revisions_dict[key][-1] self.assertIn("status", content) self.assertEqual(status, content["status"]) def assert_searchable_by_status(self, *obj_status_chain): """Assert expected status in full text search.""" objs_status_dict = {(o.type, o.id): s for o, s in obj_status_chain} full_text_properties = [] for o_type, o_id in objs_status_dict: full_text_properties.append( mysql.MysqlRecordProperty.query.filter( mysql.MysqlRecordProperty.key == o_id, mysql.MysqlRecordProperty.type == o_type, mysql.MysqlRecordProperty.property == "task status", )) query = full_text_properties[0].union(*full_text_properties[1:]) full_text_dict = {(f.type, f.key): f.content for f in query} for key, status in objs_status_dict.iteritems(): self.assertIn(key, full_text_dict) self.assertEqual(status, full_text_dict[key]) def assert_status_over_bulk_update(self, statuses, assert_statuses): """Assertion cycle_task statuses over bulk update.""" self._update_ct_via_patch(statuses) self.refresh_set_up_instances() self.assertItemsEqual(assert_statuses, [obj.status for obj in self.tasks]) obj_status_chain = [(t, assert_statuses[idx]) for idx, t in enumerate(self.tasks)] self.assert_latest_revision_status(*obj_status_chain) self.assert_searchable_by_status(*obj_status_chain) def test_propagation_status_full(self): """Task status propagation for required verification workflow.""" # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 3, [self.IN_PROGRESS] * 3) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update( [self.FINISHED], [self.FINISHED, self.IN_PROGRESS, self.IN_PROGRESS]) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 3, [self.FINISHED] * 3) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u'manual_cycle_created') # all tasks moved to verified self.assert_status_over_bulk_update([self.VERIFIED] * 3, [self.VERIFIED] * 3) self.assertEqual(self.VERIFIED, self.group.status) self.assertEqual(self.VERIFIED, self.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') def test_propagation_status_short(self): """Task status propagation for not required verification workflow.""" all_models.Cycle.query.filter( all_models.Cycle.id == self.cycle.id).update( {all_models.Cycle.is_verification_needed: False}) db.session.commit() self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id).all() # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 3, [self.IN_PROGRESS] * 3) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update( [self.FINISHED], [self.FINISHED, self.IN_PROGRESS, self.IN_PROGRESS]) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assert_notifications_for_object(self.tasks[0]) for task in self.tasks[1:]: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u'manual_cycle_created') # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 3, [self.FINISHED] * 3) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.cycle = self.tasks[0].cycle self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') def test_deprecated_final(self): """Test task status propagation for deprecated workflow.""" self.assertEqual([self.ASSIGNED] * 3, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.DEPRECATED] * 3, [self.DEPRECATED] * 3) self.assertEqual(self.DEPRECATED, self.group.status) self.assertEqual(self.DEPRECATED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assert_status_over_bulk_update( [self.ASSIGNED, self.VERIFIED, self.FINISHED], [self.DEPRECATED] * 3) self.assertEqual(self.DEPRECATED, self.group.status) self.assertEqual(self.DEPRECATED, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) @ddt.data({ "to_state": FINISHED, "success": True }, { "to_state": IN_PROGRESS, "success": False }, { "to_state": ASSIGNED, "success": False }, { "to_state": VERIFIED, "success": False }) @ddt.unpack def test_move_from_declined(self, to_state, success): """Test status propagation over update declined tasks to {to_state}.""" self.assertEqual([self.ASSIGNED] * len(self.tasks), [t.status for t in self.tasks]) start_state = self.DECLINED start_statuses = [start_state] * len(self.tasks) with factories.single_commit(): for idx, status in enumerate(start_statuses): self.tasks[idx].status = status self.group.status = self.IN_PROGRESS self.cycle.status = self.IN_PROGRESS self.workflow.status = all_models.Workflow.ACTIVE self.assertEqual(start_statuses, [t.status for t in self.tasks]) # all tasks in progress state to_states = [to_state] * len(self.tasks) self._update_ct_via_patch(to_states) self.refresh_set_up_instances() if success: task_state = group_state = cycle_state = to_state else: task_state = start_state group_state = cycle_state = self.IN_PROGRESS self.assertEqual([task_state] * len(self.tasks), [t.status for t in self.tasks]) self.assertEqual(group_state, self.group.status) self.assertEqual(cycle_state, self.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) def test_update_tasks_from_2_cycles(self): """Test bulk update few cycles at the same time.""" with factories.single_commit(): _, _, group, _ = self._create_cycle_structure() group_id = group.id all_models.Cycle.query.update( {all_models.Cycle.is_verification_needed: False}) db.session.commit() self.tasks = all_models.CycleTaskGroupObjectTask.query.order_by( all_models.CycleTaskGroupObjectTask.id).all() # all tasks in assigned state self.assertEqual([self.ASSIGNED] * 6, [t.status for t in self.tasks]) # all tasks in progress state self.assert_status_over_bulk_update([self.IN_PROGRESS] * 6, [self.IN_PROGRESS] * 6) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(self.IN_PROGRESS, group.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.ACTIVE, group.cycle.workflow.status) # update 1 task to finished self.assert_status_over_bulk_update([self.FINISHED], [self.FINISHED] + [self.IN_PROGRESS] * 5) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.IN_PROGRESS, self.group.status) self.assertEqual(self.IN_PROGRESS, group.status) self.assertEqual(self.IN_PROGRESS, self.cycle.status) self.assertEqual(self.IN_PROGRESS, group.cycle.status) self.assertEqual(all_models.Workflow.ACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.ACTIVE, group.cycle.workflow.status) self.assert_notifications_for_object(self.tasks[0]) for task in self.tasks[1:]: self.assert_notifications_for_object(task, u'cycle_task_due_in', u'cycle_task_due_today', u'cycle_task_overdue', u'manual_cycle_created') self.assert_notifications_for_object(self.group.cycle, u'manual_cycle_created') self.assert_notifications_for_object(group.cycle, u'manual_cycle_created') # all tasks moved to finished self.assert_status_over_bulk_update([self.FINISHED] * 6, [self.FINISHED] * 6) group = all_models.CycleTaskGroup.query.get(group_id) self.assertEqual(self.FINISHED, self.group.status) self.assertEqual(self.FINISHED, group.status) self.assertEqual(self.FINISHED, self.cycle.status) self.assertEqual(self.FINISHED, group.cycle.status) self.assertEqual(all_models.Workflow.INACTIVE, self.workflow.status) self.assertEqual(all_models.Workflow.INACTIVE, group.cycle.workflow.status) for task in self.tasks: self.assert_notifications_for_object(task) self.assert_notifications_for_object(self.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created') self.assert_notifications_for_object(group.cycle, u"all_cycle_tasks_completed", u'manual_cycle_created')
class TestStatusApiPatch(TestCase): """Test CycleTask's statuses change via PATCH operation.""" ASSIGNED = all_models.CycleTaskGroupObjectTask.ASSIGNED IN_PROGRESS = all_models.CycleTaskGroupObjectTask.IN_PROGRESS FINISHED = all_models.CycleTaskGroupObjectTask.FINISHED DEPRECATED = all_models.CycleTaskGroupObjectTask.DEPRECATED def setUp(self): super(TestStatusApiPatch, self).setUp() self.api = Api() with factories.single_commit(): self.assignee = factories.PersonFactory( email="*****@*****.**") self.workflow = wf_factories.WorkflowFactory() self.cycle = wf_factories.CycleFactory(workflow=self.workflow) self.group = wf_factories.CycleTaskGroupFactory( cycle=self.cycle, context=self.cycle.workflow.context) self.tasks = [] for ind in xrange(3): self.tasks.append( wf_factories.CycleTaskFactory( title='task{}'.format(ind), cycle=self.cycle, cycle_task_group=self.group, context=self.cycle.workflow.context)) # Emulate that current user is assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfo_or_assignee = ( MagicMock(return_value=True)) def _update_ct_via_patch(self, new_states): """Update CycleTasks' state via PATCH. Args: new_states: List of states to which CTs should be moved via PATCH. Returns: JSON response which was returned by back-end. """ data = [{ "state": state, "id": self.tasks[ind].id } for ind, state in enumerate(new_states)] resp = self.api.patch(all_models.CycleTaskGroupObjectTask, data) return resp.json def _get_exp_response(self, exp_res): """Format expected response from CycleTasks' test data indexes. Args: exp_res: Expected results dict: {test_ctask_index: response_status} Returns: [{'id': updated_task_id, 'status': 'success|skipped|error'}, ...] """ return [{ 'id': self.tasks[ind].id, 'status': status } for ind, status in exp_res.iteritems()] @ddt.data( ( # New statuses which we try to set during the test [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS], # Expected results: {test_ctask_index: response_status} { 0: 'updated', 1: 'updated', 2: 'updated' }, # Expected states after update [IN_PROGRESS, IN_PROGRESS, IN_PROGRESS]), ([DEPRECATED, DEPRECATED, DEPRECATED], { 0: 'updated', 1: 'updated', 2: 'updated' }, [DEPRECATED, DEPRECATED, DEPRECATED]), ([FINISHED, FINISHED, FINISHED], { 0: 'skipped', 1: 'skipped', 2: 'skipped' }, [ASSIGNED, ASSIGNED, ASSIGNED])) @ddt.unpack def test_ct_status_bulk_update_ok(self, new_states, exp_res, exp_states): """Check CycleTasks' state update with valid data via PATCH.""" self.assertItemsEqual(self._get_exp_response(exp_res), self._update_ct_via_patch(new_states)) self.assertItemsEqual( exp_states, [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_json_error(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid data via PATCH.""" exp_resp = { "message": "Request's JSON contains multiple statuses for CycleTasks", "code": 400 } self.assertEqual( exp_resp, self._update_ct_via_patch( [self.IN_PROGRESS, self.FINISHED, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_without_permissions(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update without permissions via PATCH.""" # Emulate that current user is not assignee for all test CycleTasks. all_models.CycleTaskGroupObjectTask.current_user_wfo_or_assignee = ( MagicMock(return_value=False)) self.assertItemsEqual( self._get_exp_response({ 0: 'skipped', 1: 'skipped', 2: 'skipped' }), self._update_ct_via_patch( [self.IN_PROGRESS, self.IN_PROGRESS, self.IN_PROGRESS])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query]) def test_ct_status_bulk_update_invalid_state(self): # noqa pylint: disable=invalid-name """Check CycleTasks' state update with invalid statuses via PATCH.""" exp_resp = { "message": "Request's JSON contains invalid statuses for CycleTasks", "code": 400 } self.assertItemsEqual( exp_resp, self._update_ct_via_patch(['InVaLiD', 'InVaLiD'])) self.assertItemsEqual( [self.ASSIGNED, self.ASSIGNED, self.ASSIGNED], [obj.status for obj in all_models.CycleTaskGroupObjectTask.query])