Ejemplo n.º 1
0
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')
Ejemplo n.º 2
0
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')
Ejemplo n.º 3
0
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])