text: # coding: utf-8
  #
  # Copyright 2014 The Oppia Authors. All Rights Reserved.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS-IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  """Tests for draft upgrade services."""
  
  from __future__ import absolute_import  # pylint: disable=import-only-modules
  from __future__ import unicode_literals  # pylint: disable=import-only-modules
  
  from core.domain import draft_upgrade_services
  from core.domain import exp_domain
  from core.domain import exp_fetchers
  from core.domain import exp_services
  from core.tests import test_utils
  import feconf
  import python_utils
  import utils
  
  
  class DraftUpgradeUnitTests(test_utils.GenericTestBase):
      """Test the draft upgrade services module."""
  
      EXP_ID = 'exp_id'
      USER_ID = 'user_id'
      OTHER_CHANGE_LIST = [exp_domain.ExplorationChange({
          'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
          'property_name': 'title',
          'new_value': 'New title'
      })]
      EXP_MIGRATION_CHANGE_LIST = [exp_domain.ExplorationChange({
          'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
          'from_version': '0',
          'to_version': python_utils.UNICODE(
              feconf.CURRENT_STATE_SCHEMA_VERSION)
      })]
      DRAFT_CHANGELIST = [exp_domain.ExplorationChange({
          'cmd': 'edit_exploration_property',
          'property_name': 'title',
          'old_value': None,
          'new_value': 'Updated title'})]
  
      def setUp(self):
          super(DraftUpgradeUnitTests, self).setUp()
          self.save_new_valid_exploration(self.EXP_ID, self.USER_ID)
  
      def test_try_upgrade_with_no_version_difference(self):
          self.assertIsNone(
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  self.DRAFT_CHANGELIST, 1, 1, self.EXP_ID))
  
      def test_try_upgrade_raises_exception_if_versions_are_invalid(self):
          with self.assertRaisesRegexp(
              utils.InvalidInputException,
              'Current draft version is greater than the exploration version.'):
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  self.DRAFT_CHANGELIST, 2, 1, self.EXP_ID)
  
          exp_services.update_exploration(
              self.USER_ID, self.EXP_ID, self.OTHER_CHANGE_LIST,
              'Changed exploration title.')
          exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID)
          self.assertEqual(exploration.version, 2)
          self.assertIsNone(
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID))
  
      def test_try_upgrade_failure_due_to_unsupported_commit_type(self):
          exp_services.update_exploration(
              self.USER_ID, self.EXP_ID, self.OTHER_CHANGE_LIST,
              'Changed exploration title.')
          exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID)
          self.assertEqual(exploration.version, 2)
          self.assertIsNone(
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID))
  
      def test_try_upgrade_failure_due_to_unimplemented_upgrade_methods(self):
          exp_services.update_exploration(
              self.USER_ID, self.EXP_ID, self.EXP_MIGRATION_CHANGE_LIST,
              'Ran Exploration Migration job.')
          exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID)
          self.assertEqual(exploration.version, 2)
          self.assertIsNone(
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID))
  
  
  class DraftUpgradeUtilUnitTests(test_utils.GenericTestBase):
      """Test the DraftUpgradeUtil module."""
  
      EXP_ID = 'exp_id'
      USER_ID = 'user_id'
      EXP_MIGRATION_CHANGE_LIST = [exp_domain.ExplorationChange({
          'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
          'from_version': '36',
          'to_version': '37'
      })]
  
      # EXP_ID and USER_ID used to create default explorations.
      EXP_ID = 'exp_id'
      USER_ID = 'user_id'
  
      def create_and_migrate_new_exploration(
              self, current_schema_version, target_schema_version):
          """Creates an exploration and applies a state schema migration to it.
  
          Creates an exploration and migrates its state schema from version
          current_schema_version to target_schema_version. Asserts that the
          exploration was successfully migrated.
  
          Args:
              current_schema_version: string. The current schema version of the
                  exploration (eg. '29').
              target_schema_version: string. The schema version to upgrade
                  the exploration to (eg. '30').
          """
  
          # Create an exploration change list with the command that will migrate
          # the schema from current_schema_version to target_schema_version.
          exp_migration_change_list = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
                  'from_version': current_schema_version,
                  'to_version': target_schema_version
              })
          ]
  
          # The migration will automatically migrate the exploration to the latest
          # state schema version, so we set the latest schema version to be the
          # target_schema_version.
          with self.swap(
              feconf, 'CURRENT_STATE_SCHEMA_VERSION',
              int(target_schema_version)):
  
              # Create and migrate the exploration.
              self.save_new_valid_exploration(self.EXP_ID, self.USER_ID)
              exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID)
              exp_services.update_exploration(
                  self.USER_ID, self.EXP_ID, exp_migration_change_list,
                  'Ran Exploration Migration job.')
  
              # Assert that the update was applied and that the exploration state
              # schema was successfully updated.
              exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID)
              self.assertEqual(exploration.version, 2)
              self.assertEqual(
                  python_utils.UNICODE(
                      exploration.states_schema_version),
                  target_schema_version)
  
      def test_convert_to_latest_schema_version_implemented(self):
          state_schema_version = feconf.CURRENT_STATE_SCHEMA_VERSION
          conversion_fn_name = '_convert_states_v%s_dict_to_v%s_dict' % (
              state_schema_version - 1, state_schema_version)
          self.assertTrue(
              hasattr(
                  draft_upgrade_services.DraftUpgradeUtil, conversion_fn_name),
              msg='Current schema version is %d but DraftUpgradeUtil.%s is '
              'unimplemented.' % (state_schema_version, conversion_fn_name))
  
      def test_convert_states_v44_dict_to_v45_dict(self):
          draft_change_list_v44 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 45.
          self.create_and_migrate_new_exploration('44', '45')
          migrated_draft_change_list_v45 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v44, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v44_dict_list = [
              change.to_dict() for change in draft_change_list_v44
          ]
          migrated_draft_change_list_v45_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v45
          ]
          self.assertEqual(
              draft_change_list_v44_dict_list,
              migrated_draft_change_list_v45_dict_list)
  
      def test_convert_states_v43_dict_to_v44_dict(self):
          draft_change_list_v43 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Introduction',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 44.
          self.create_and_migrate_new_exploration('43', '44')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema. In this case there are no change to the
          # draft change list since version 44 adds the
          # card_is_checkpoint boolean variable to the exploration
          # state, for which there should be no changes to drafts.
          migrated_draft_change_list_v44 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v43, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v43_dict_list = [
              change.to_dict() for change in draft_change_list_v43
          ]
          migrated_draft_change_list_v44_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v44
          ]
          self.assertEqual(
              draft_change_list_v43_dict_list,
              migrated_draft_change_list_v44_dict_list)
  
      def test_convert_states_v42_dict_to_v43_dict(self):
          draft_change_list_1_v42 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': {}
              })
          ]
          draft_change_list_2_v42 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              })
          ]
          # Migrate exploration to state schema version 43.
          self.create_and_migrate_new_exploration('42', '43')
          migrated_draft_change_list_1_v43 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v42, 1, 2, self.EXP_ID))
          # Verify that changes which include answer groups are
          # not upgraded to v42.
          self.assertIsNone(migrated_draft_change_list_1_v43)
  
          migrated_draft_change_list_2_v43 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v42, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v42_dict_list = [
              change.to_dict() for change in draft_change_list_2_v42
          ]
          migrated_draft_change_list_2_v43_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v43
          ]
          # Verify that changes which do not include answer groups can
          # be upgraded to v43.
          self.assertEqual(
              draft_change_list_2_v42_dict_list,
              migrated_draft_change_list_2_v43_dict_list)
  
      def test_convert_states_v41_dict_to_v42_dict(self):
          draft_change_list_1_v41 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': {}
              })
          ]
          draft_change_list_2_v41 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              })
          ]
          # Migrate exploration to state schema version 42.
          self.create_and_migrate_new_exploration('41', '42')
          migrated_draft_change_list_1_v42 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v41, 1, 2, self.EXP_ID))
          # Verify that changes which include answer groups are
          # not upgraded to v41.
          self.assertIsNone(migrated_draft_change_list_1_v42)
  
          migrated_draft_change_list_2_v42 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v41, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v41_dict_list = [
              change.to_dict() for change in draft_change_list_2_v41
          ]
          migrated_draft_change_list_2_v42_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v42
          ]
          # Verify that changes which do not include answer groups can
          # be upgraded to v42.
          self.assertEqual(
              draft_change_list_2_v41_dict_list,
              migrated_draft_change_list_2_v42_dict_list)
  
      def test_convert_states_v40_dict_to_v41_dict(self):
          draft_change_list_1_v40 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': {}
              })
          ]
          draft_change_list_2_v40 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              })
          ]
          # Migrate exploration to state schema version 41.
          self.create_and_migrate_new_exploration('40', '41')
          migrated_draft_change_list_1_v41 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v40, 1, 2, self.EXP_ID))
          # Verify that changes which include answer groups are
          # not upgraded to v41.
          self.assertIsNone(migrated_draft_change_list_1_v41)
  
          migrated_draft_change_list_2_v41 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v40, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v40_dict_list = [
              change.to_dict() for change in draft_change_list_2_v40
          ]
          migrated_draft_change_list_2_v41_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v41
          ]
          # Verify that changes which do not include answer groups can
          # be upgraded to v41.
          self.assertEqual(
              draft_change_list_2_v40_dict_list,
              migrated_draft_change_list_2_v41_dict_list)
  
      def test_convert_states_v39_dict_to_v40_dict(self):
          draft_change_list_1_v39 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_customization_args',
                  'new_value': {}
              })
          ]
          draft_change_list_2_v39 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              })
          ]
          # Migrate exploration to state schema version 40.
          self.create_and_migrate_new_exploration('39', '40')
          migrated_draft_change_list_1_v40 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v39, 1, 2, self.EXP_ID))
          # Verify that changes which include customization arguments are
          # not upgraded to v40.
          self.assertIsNone(migrated_draft_change_list_1_v40)
  
          migrated_draft_change_list_2_v40 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v39, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v39_dict_list = [
              change.to_dict() for change in draft_change_list_2_v39
          ]
          migrated_draft_change_list_2_v40_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v40
          ]
          # Verify that changes which do not include customization arguments can
          # be upgraded to v40.
          self.assertEqual(
              draft_change_list_2_v39_dict_list,
              migrated_draft_change_list_2_v40_dict_list)
  
      def test_convert_states_v38_dict_to_v39_dict(self):
          draft_change_list_v38 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 39.
          self.create_and_migrate_new_exploration('38', '39')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema. In this case there are no changes to the
          # draft change list since version 39 adds a customization arg
          # for modifying the placeholder text in the Numeric Expression Input
          # interaction, for which there should be no changes to drafts.
          migrated_draft_change_list_v39 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v38, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v38_dict_list = [
              change.to_dict() for change in draft_change_list_v38
          ]
          migrated_draft_change_list_v39_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v39
          ]
          self.assertEqual(
              draft_change_list_v38_dict_list,
              migrated_draft_change_list_v39_dict_list)
  
      def test_convert_states_v37_dict_to_v38_dict(self):
          draft_change_list_v37 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 38.
          self.create_and_migrate_new_exploration('37', '38')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema. In this case there are no changes to the
          # draft change list since version 38 adds a customization arg
          # for the "Add" button text in SetInput interaction for the
          # exploration, for which there should be no changes to drafts.
          migrated_draft_change_list_v38 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v37, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v37_dict_list = [
              change.to_dict() for change in draft_change_list_v37
          ]
          migrated_draft_change_list_v38_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v38
          ]
          self.assertEqual(
              draft_change_list_v37_dict_list,
              migrated_draft_change_list_v38_dict_list)
  
      def test_convert_states_v36_dict_to_v37_dict(self):
          draft_change_list_v36 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': 'edit_state_property',
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': [{
                      'rule_specs': [{
                          'rule_type': 'CaseSensitiveEquals',
                          'inputs': {
                              'x': 'test'
                          }
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': '<p>Content</p>'
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }]
              })
          ]
          draft_change_list_v37 = [
              exp_domain.ExplorationChange({
                  'cmd': 'edit_state_property',
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': 'edit_state_property',
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': [{
                      'rule_specs': [{
                          'rule_type': 'Equals',
                          'inputs': {
                              'x': 'test'
                          }
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': '<p>Content</p>'
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }]
              })
          ]
  
          # Migrate exploration to state schema version 37.
          self.create_and_migrate_new_exploration('36', '37')
          migrated_draft_change_list_v37 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v36, 1, 2, self.EXP_ID))
  
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v37_dict_list = [
              change.to_dict() for change in draft_change_list_v37
          ]
          migrated_draft_change_list_v37_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v37
          ]
          self.assertEqual(
              draft_change_list_v37_dict_list,
              migrated_draft_change_list_v37_dict_list)
  
      def test_convert_states_v35_dict_to_v36_dict(self):
          draft_change_list_1_v35 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_customization_args',
                  'new_value': {}
              })
          ]
          draft_change_list_2_v35 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              })
          ]
          # Migrate exploration to state schema version 36.
          self.create_and_migrate_new_exploration('35', '36')
          migrated_draft_change_list_1_v36 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v35, 1, 2, self.EXP_ID))
          self.assertIsNone(migrated_draft_change_list_1_v36)
  
          migrated_draft_change_list_2_v36 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v35, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v35_dict_list = [
              change.to_dict() for change in draft_change_list_2_v35
          ]
          migrated_draft_change_list_2_v36_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v36
          ]
          self.assertEqual(
              draft_change_list_2_v35_dict_list,
              migrated_draft_change_list_2_v36_dict_list)
  
      def test_convert_states_v34_dict_to_v35_dict(self):
          draft_change_list_1_v34 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'widget_id',
                  'new_value': 'MathExpressionInput'
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'answer_groups',
                  'new_value': [{
                      'rule_specs': [{
                          'rule_type': 'IsMathematicallyEquivalentTo',
                          'inputs': {
                              'x': 'x+y/2'
                          }
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': '<p>Content</p>'
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }]
              })
          ]
          draft_change_list_2_v34 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 35.
          self.create_and_migrate_new_exploration('34', '35')
          migrated_draft_change_list_1_v35 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_1_v34, 1, 2, self.EXP_ID))
          self.assertIsNone(migrated_draft_change_list_1_v35)
  
          migrated_draft_change_list_2_v35 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_2_v34, 1, 2, self.EXP_ID))
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_2_v34_dict_list = [
              change.to_dict() for change in draft_change_list_2_v34
          ]
          migrated_draft_change_list_2_v35_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_2_v35
          ]
          self.assertEqual(
              draft_change_list_2_v34_dict_list,
              migrated_draft_change_list_2_v35_dict_list)
  
      def test_convert_states_v33_dict_to_v34_dict(self):
          html_content = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
  
          expected_html_content = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
  
          draft_change_list = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state2',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              html_content,
                              '<p>4</p>'
                          ]
                      },
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      }
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'answer_groups',
                  'state_name': 'State 1',
                  'new_value': [{
                      'rule_specs': [{
                          'rule_type': 'Equals',
                          'inputs': {
                              'x': [html_content]
                          }
                      }, {
                          'rule_type': 'ContainsAtLeastOneOf',
                          'inputs': {
                              'x': [html_content]
                          }
                      }, {
                          'rule_type': 'IsProperSubsetOf',
                          'inputs': {
                              'x': [html_content]
                          }
                      }, {
                          'rule_type': 'DoesNotContainAtLeastOneOf',
                          'inputs': {
                              'x': [html_content]
                          }
                      }, {
                          'rule_type': 'Equals',
                          'inputs': {
                              'x': 1
                          }
                      }, {
                          'rule_type': 'HasElementXAtPositionY',
                          'inputs': {
                              'x': html_content,
                              'y': 2
                          }
                      }, {
                          'rule_type': 'IsEqualToOrdering',
                          'inputs': {
                              'x': [[html_content]]
                          }
                      }, {
                          'rule_type': 'HasElementXBeforeElementY',
                          'inputs': {
                              'x': html_content,
                              'y': html_content
                          }
                      }, {
                          'rule_type': (
                              'IsEqualToOrderingWithOneItemAtIncorrectPosition'),
                          'inputs': {
                              'x': [[html_content]]
                          }
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': html_content
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }]
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': {
                      'content_id': 'content',
                      'html': html_content
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'written_translations',
                  'new_value': {
                      'translations_mapping': {
                          'content1': {
                              'en': {
                                  'html': html_content,
                                  'needs_update': True
                              },
                              'hi': {
                                  'html': 'Hey!',
                                  'needs_update': False
                              }
                          },
                          'feedback_1': {
                              'hi': {
                                  'html': html_content,
                                  'needs_update': False
                              },
                              'en': {
                                  'html': 'hello!',
                                  'needs_update': False
                              }
                          }
                      }
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'solution',
                  'new_value': {
                      'answer_is_exclusive': False,
                      'correct_answer': 'helloworld!',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_content
                      },
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'solution',
                  'new_value': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [html_content],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'default_outcome',
                  'new_value': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_content
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  }
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'hints',
                  'new_value': [{
                      'hint_content': {
                          'content_id': 'hint1',
                          'html': html_content
                      }
                  }]
              }), exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_RENAME_STATE,
                  'old_state_name': 'Intro',
                  'new_state_name': 'Introduction',
              })
          ]
  
          self.create_and_migrate_new_exploration('33', '34')
          migrated_draft_change_list = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list, 1, 2, self.EXP_ID))
          self.assertEqual(
              migrated_draft_change_list[0].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state2',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              expected_html_content,
                              '<p>4</p>'
                          ]
                      },
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      }
                  }
              }).to_dict())
  
          self.assertEqual(
              migrated_draft_change_list[1].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'answer_groups',
                  'state_name': 'State 1',
                  'new_value': [{
                      'rule_specs': [{
                          'rule_type': 'Equals',
                          'inputs': {
                              'x': [expected_html_content]
                          }
                      }, {
                          'rule_type': 'ContainsAtLeastOneOf',
                          'inputs': {
                              'x': [expected_html_content]
                          }
                      }, {
                          'rule_type': 'IsProperSubsetOf',
                          'inputs': {
                              'x': [expected_html_content]
                          }
                      }, {
                          'rule_type': 'DoesNotContainAtLeastOneOf',
                          'inputs': {
                              'x': [expected_html_content]
                          }
                      }, {
                          'rule_type': 'Equals',
                          'inputs': {
                              'x': 1
                          }
                      }, {
                          'rule_type': 'HasElementXAtPositionY',
                          'inputs': {
                              'x': expected_html_content,
                              'y': 2
                          }
                      }, {
                          'rule_type': 'IsEqualToOrdering',
                          'inputs': {
                              'x': [[expected_html_content]]
                          }
                      }, {
                          'rule_type': 'HasElementXBeforeElementY',
                          'inputs': {
                              'x': expected_html_content,
                              'y': expected_html_content
                          }
                      }, {
                          'rule_type': (
                              'IsEqualToOrderingWithOneItemAtIncorrectPosition'),
                          'inputs': {
                              'x': [[expected_html_content]]
                          }
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': expected_html_content
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }]
              }).to_dict())
          self.assertEqual(
              migrated_draft_change_list[2].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': {
                      'content_id': 'content',
                      'html': expected_html_content
                  }
              }).to_dict())
          self.assertEqual(
              migrated_draft_change_list[3].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'written_translations',
                  'new_value': {
                      'translations_mapping': {
                          'content1': {
                              'en': {
                                  'html': expected_html_content,
                                  'needs_update': True
                              },
                              'hi': {
                                  'html': 'Hey!',
                                  'needs_update': False
                              }
                          },
                          'feedback_1': {
                              'hi': {
                                  'html': expected_html_content,
                                  'needs_update': False
                              },
                              'en': {
                                  'html': 'hello!',
                                  'needs_update': False
                              }
                          }
                      }
                  }
              }).to_dict())
          self.assertEqual(
              migrated_draft_change_list[4].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'solution',
                  'new_value': {
                      'answer_is_exclusive': False,
                      'correct_answer': 'helloworld!',
                      'explanation': {
                          'content_id': 'solution',
                          'html': expected_html_content
                      },
                  }
              }).to_dict())
          self.assertEqual(
              migrated_draft_change_list[5].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'solution',
                  'new_value': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [expected_html_content],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
              }).to_dict())
  
          self.assertEqual(
              migrated_draft_change_list[6].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'default_outcome',
                  'new_value': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': expected_html_content
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  }
              }).to_dict())
  
          self.assertEqual(
              migrated_draft_change_list[7].to_dict(),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'hints',
                  'new_value': [{
                      'hint_content': {
                          'content_id': 'hint1',
                          'html': expected_html_content
                      }
                  }]
              }).to_dict())
  
      def test_convert_states_v32_dict_to_v33_dict(self):
          draft_change_list_v32 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state1',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              '<p>3</p>',
                              '<p>4</p>'
                          ]
                      }
                  }
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state2',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              '<p>3</p>',
                              '<p>4</p>'
                          ]
                      },
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      }
                  }
              })
          ]
          # Version 33 adds a showChoicesInShuffledOrder bool, which doesn't
          # impact the second ExplorationChange because it will only impact
          # it if 'choices' is the only key for new_value.
          expected_draft_change_list_v33 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state1',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              '<p>3</p>',
                              '<p>4</p>'
                          ]
                      },
                      'showChoicesInShuffledOrder': {
                          'value': False
                      }
                  }
              }),
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'state2',
                  'property_name': 'widget_customization_args',
                  'new_value': {
                      'choices': {
                          'value': [
                              '<p>1</p>',
                              '<p>2</p>',
                              '<p>3</p>',
                              '<p>4</p>'
                          ]
                      },
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      }
                  }
              })
          ]
          # Migrate exploration to state schema version 33.
          self.create_and_migrate_new_exploration('32', '33')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema.
          migrated_draft_change_list_v33 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v32, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          expected_draft_change_list_v33_dict_list = [
              change.to_dict() for change in expected_draft_change_list_v33
          ]
          migrated_draft_change_list_v33_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v33
          ]
          self.assertEqual(
              expected_draft_change_list_v33_dict_list,
              migrated_draft_change_list_v33_dict_list)
  
      def test_convert_states_v31_dict_to_v32_dict(self):
          draft_change_list_v31 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 32.
          self.create_and_migrate_new_exploration('31', '32')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema. In this case there are no changes to the
          # draft change list since version 32 adds a customization arg
          # for the "Add" button text in SetInput interaction for the
          # exploration, for which there should be no changes to drafts.
          migrated_draft_change_list_v32 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v31, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v31_dict_list = [
              change.to_dict() for change in draft_change_list_v31
          ]
          migrated_draft_change_list_v32_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v32
          ]
          self.assertEqual(
              draft_change_list_v31_dict_list,
              migrated_draft_change_list_v32_dict_list)
  
      def test_convert_states_v30_dict_to_v31_dict(self):
          draft_change_list_v30 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'recorded_voiceovers',
                  'new_value': {
                      'voiceovers_mapping': {
                          'content': {
                              'en': {
                                  'file_size_name': 100,
                                  'filename': 'atest.mp3',
                                  'needs_update': False
                              }
                          }
                      }
                  }
              })
          ]
          # Version 31 adds the duration_secs property.
          expected_draft_change_list_v31 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'recorded_voiceovers',
                  'new_value': {
                      'voiceovers_mapping': {
                          'content': {
                              'en': {
                                  'file_size_name': 100,
                                  'filename': 'atest.mp3',
                                  'needs_update': False,
                                  'duration_secs': 0.0
                              }
                          }
                      }
                  }
              })
          ]
          # Migrate exploration to state schema version 31.
          self.create_and_migrate_new_exploration('30', '31')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema.
          migrated_draft_change_list_v31 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v30, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          expected_draft_change_list_v31_dict_list = [
              change.to_dict() for change in expected_draft_change_list_v31
          ]
          migrated_draft_change_list_v31_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v31
          ]
          self.assertEqual(
              expected_draft_change_list_v31_dict_list,
              migrated_draft_change_list_v31_dict_list)
  
      def test_convert_states_v29_dict_to_v30_dict(self):
          draft_change_list_v29 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'answer_groups',
                  'state_name': 'State 1',
                  'new_value': {
                      'rule_specs': [{
                          'rule_type': 'Equals',
                          'inputs': {'x': [
                              '<p>This is value1 for ItemSelection</p>'
                          ]}
                      }, {
                          'rule_type': 'Equals',
                          'inputs': {'x': [
                              '<p>This is value2 for ItemSelection</p>'
                          ]}
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': '<p>Outcome for state1</p>'
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_misconception_id': None
                  }
              })
          ]
          # Version 30 replaces the tagged_misconception_id in version 29
          # with tagged_skill_misconception_id.
          expected_draft_change_list_v30 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'answer_groups',
                  'state_name': 'State 1',
                  'new_value': {
                      'rule_specs': [{
                          'rule_type': 'Equals',
                          'inputs': {'x': [
                              '<p>This is value1 for ItemSelection</p>'
                          ]}
                      }, {
                          'rule_type': 'Equals',
                          'inputs': {'x': [
                              '<p>This is value2 for ItemSelection</p>'
                          ]}
                      }],
                      'outcome': {
                          'dest': 'Introduction',
                          'feedback': {
                              'content_id': 'feedback',
                              'html': '<p>Outcome for state1</p>'
                          },
                          'param_changes': [],
                          'labelled_as_correct': False,
                          'refresher_exploration_id': None,
                          'missing_prerequisite_skill_id': None
                      },
                      'training_data': [],
                      'tagged_skill_misconception_id': None
                  }
              })
          ]
          # Migrate exploration to state schema version 30.
          self.create_and_migrate_new_exploration('29', '30')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema.
          migrated_draft_change_list_v30 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v29, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          expected_draft_change_list_v30_dict_list = [
              change.to_dict() for change in expected_draft_change_list_v30
          ]
          migrated_draft_change_list_v30_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v30
          ]
          self.assertEqual(
              expected_draft_change_list_v30_dict_list,
              migrated_draft_change_list_v30_dict_list)
  
      def test_convert_states_v28_dict_to_v29_dict(self):
          draft_change_list_v28 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'state_name': 'Intro',
                  'property_name': 'content',
                  'new_value': 'new value'
              })
          ]
          # Migrate exploration to state schema version 29.
          self.create_and_migrate_new_exploration('28', '29')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema. In this case there are no change to the
          # draft change list since version 29 adds the
          # solicit_answer_details boolean variable to the exploration
          # state, for which there should be no changes to drafts.
          migrated_draft_change_list_v29 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v28, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          draft_change_list_v28_dict_list = [
              change.to_dict() for change in draft_change_list_v28
          ]
          migrated_draft_change_list_v29_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v29
          ]
          self.assertEqual(
              draft_change_list_v28_dict_list,
              migrated_draft_change_list_v29_dict_list)
  
      def test_convert_states_v27_dict_to_v28_dict(self):
          draft_change_list_v27 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'content_ids_to_audio_translations',
                  'state_name': 'State B',
                  'new_value': 'new value',
              })
          ]
          # Version 28 adds voiceovers_mapping.
          expected_draft_change_list_v28 = [
              exp_domain.ExplorationChange({
                  'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                  'property_name': 'recorded_voiceovers',
                  'state_name': 'State B',
                  'new_value': {'voiceovers_mapping': 'new value'}
              })
          ]
          # Migrate exploration to state schema version 28.
          self.create_and_migrate_new_exploration('27', '28')
          # Migrate the draft change list's state schema to the migrated
          # exploration's schema.
          migrated_draft_change_list_v28 = (
              draft_upgrade_services.try_upgrading_draft_to_exp_version(
                  draft_change_list_v27, 1, 2, self.EXP_ID)
          )
          # Change draft change lists into a list of dicts so that it is
          # easy to compare the whole draft change list.
          expected_draft_change_list_v28_dict_list = [
              change.to_dict() for change in expected_draft_change_list_v28
          ]
          migrated_draft_change_list_v28_dict_list = [
              change.to_dict() for change in migrated_draft_change_list_v28
          ]
          self.assertEqual(
              expected_draft_change_list_v28_dict_list,
              migrated_draft_change_list_v28_dict_list)
  