Repository: oppia/oppia

Issue:
Fix tests/methods that refer to private functions in Python
There are currently a few tests that reference private functions in Python. Tests should test through public interface.  If the test tests a private method, find a way to test the method through the existing public interface instead (you'll need to find the corresponding public API function that uses the internal private one, and use that function instead in tests). Please note that modifications to other classes outside of the file in question may be necessary. 

Note that you can identify such references by searching for the string `# pylint: disable=protected-access`. (If you don't see the string in the file(s) you're assigned to, please let the author of this issue know so that the list can be edited accordingly.)

**Important**: In general, do not just change methods from private to public. You'll need to find the already existing publicly-accessible function which invokes the private method and use that one instead.

The following the cases where we test private functions:

- [ ] gae_models.VersionedModel @Azrecycle4
  - delete
  - revert
  - get_version
  - get_multi_versions
  - get_snapshots_metadata
- [ ] suggestion_services_test.SuggestionServicesUnitTests @gopivaibhav
  - test_accept_suggestion_commit_message_after_updating_a_suggestion
  - test_update_translation_suggestion_to_change_translation_html
- [ ] suggestion_services_test.SuggestionIntegrationTests @hardikkat24
  - setUp
- [ ] email_manager_test.EmailRightsTest @vijaykpatel
  - test_sender_id_validation
- [ ] email_manager.BulkEmailsTests @brianlinUM
  - test_that_correct_email_is_sent
  - test_that_exception_is_raised_for_unauthorised_sender
- [ ] library_test.LibraryPageTests @brianlinUM
  - test_library_handler_for_created_explorations 
- [ ] build_test (All) @Mohitbalwani26
- [ ] suggestion_test.SuggestionUnitTests @AnuragVats007
  - setUp
- [ ] exp_services_test.ZipFileExportUnitTests (all) @ayushjaink8

Completed:

- [x] draft_upgrade_services_test.DraftUpgradeUnitTests @kaylahardie
  - test_try_upgrade_success
- [x] draft_upgrade_services_test.DraftUpgradeUtilUnitTests @kaylahardie
  - test_convert_states_v29_dict_to_v30_dict
  - test_convert_states_v28_dict_to_v29_dict
  - test_convert_states_v27_dict_to_v28_dict
- [x] gae_models_test.GenerateHashTests - @lorryaze
  - test_same_inputs_always_gives_same_hashes
  - test_different_inputs_give_different_hashes
- [x] models_test.BaseCalculationUnitTests @donosco98 (#7841)
  - test_equality_of_hashable_answers
- [x] user_services_test.LastLoginIntegrationTests @vpishuk
  - test_legacy_user
- [x] user_services_test.LastExplorationEditedIntegrationTests @vpishuk
  - test_legacy_user
  - test_last_exp_edit_time_gets_updated
- [x] user_services_test.LastExplorationCreatedIntegrationTests @vpishuk
  - test_legacy_user
  - test_last_exp_edit_time_gets_updated
- [x] config_domain_test.ConfigPropertyRegistryTests (@nishantwrp)
  - test_config_property_schemas_are_valid
- [x] email_manager.DuplicateEmailTests @Hudda
  - test_send_email_does_not_resend_if_same_hash_exists
  - test_sending_email_with_different_recipient_but_same_hash
  - test_sending_email_with_different_subject_but_same_hash
  - test_sending_email_with_different_body_but_same_hash
  - test_duplicate_emails_are_sent_after_some_time_has_elapsed
- [x] user_jobs_one_off_test.UserFirstContributionMsecOneOffJobTests - @Arnesh07 
  - test_contribution_msec_does_not_update_on_unpublished_explorations
- [x] user_jobs_one_off_test.UserLastExplorationActivityOneOffJobTests @Hudda
  - test_that_last_created_time_is_updated
- [x] html_validation_service_test.ContentMigrationTests - @cathuang20
  - test_validate_soup_for_rte
  - test_validate_customization_args_in_tag
  - test_that_last_edited_time_is_updated
  - test_that_last_edited_and_created_time_both_updated
  - test_that_last_edited_and_created_time_are_not_updated
- [x] rte_component_registry_test.RteComponentUnitTests - @alexlee311
  - test_rte_components_are_valid
- [x] extensions.interactions.base_test.InteractionUnitTests - @aazuspan
  - test_default_interactions_are_valid
- [x] collection_services_test @sudiptapradhan1181
  - _count_at_least_editable_collection_summaries
- [x] exp_services.test - @aazuspan
  - _count_at_least_editable_exploration_summaries


Discussion:
Can I try to do this?
Sure, which part would you like to do? (each major bullet point is open)
@kevinlee12 perhaps swap the bullet points with check-boxes?
good call, done!
Hi can I take **jobs_test.JobManagerUnitTests** ? 
sure, go for it!
@ankita240796 could you please add @kevinlee12 as my reviewer? Thank you
I'll go ahead and do a review :)
Hello! I'd like to try: email_manager.BulkEmailsTests
Hi @KristiKovacs, I've assigned you to `email_manager.BulkEmailsTests`. You can start working on it! :) 
Hello @DubeySandeep, I'd like to try "suggestion_services_test.SuggestionServicesUnitTests".
@rahmot, I've assigned you to `suggestion_services_test.SuggestionServicesUnitTests`.
@DubeySandeep, please can you put me through on how to get started with suggestion_services_test.SuggestionServicesUnitTests. Thank you!
@rahmot I would have approached this issue in the following way:

 After going through the details of the issue, I understand that  "Few test methods in test files are using private methods for testing, so I have to change the test methods such that it only uses the public methods."

Let's solve one test method for your case:

1. I looked into suggestion_services_test and found `test_accept_suggestion_handled_suggestion_failure` method on line 350.
2. I see line 368 is calling a private method `_update_suggestion`.
3. I read the test code and found that we are trying to change the status of the suggestion and update the suggestion using the private method from the suggestion_service.
4. Going further I read a few lines of suggestion_service and found `mark_review_completed` which does the same thing and I think that can be useful.

With all this information I will go ahead and change the test method and create a PR!

(Note: maybe this idea won't work, then you have to take a deeper look and find another way to solve the problem.)

I hope this helps.
@DubeySandeep, thanks for the illumination. I sincerely appreciate.
Hello @DubeySandeep can I take email_manager.DuplicateEmailTests ?
Hi @Hudda, I think you haven't filled the survey form yet, would you mind doing that first? Once you complete the instructions provided [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up), do let us know we will go ahead and assign you to the issue you have selcted. :) 
@DubeySandeep I have adhered to your instructions and filled the form.
@Hudda, I've assigned you `email_manager.DuplicateEmailTests`, you can go ahead and start working on it! :)  
Hello I am new to the python contribution world.  I would like to tactle 
email_manager_test.EmailRightsTest
How would I go about starting to do this task?
welcome @emzee831! Please take a look at our [contributor's guide](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) on how to get started.
Hi @rahmot, any updates here? Please feel free to let us know if you are stuck somewhere, we will be happy to help you! Thanks!
> welcome @emzee831! Please take a look at our [contributor's guide](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) on how to get started.

Just looked over the contribution guide and filled out all the information. 
Hi, @DubeySandeep  can I work on  jobs_test.JobRegistryTests ?
HI! I'm an Outreachy applicant interested in the 'print-to-pdf' project. Can I work on  draft_upgrade_services_test.DraftUpgradeUtilUnitTests ?
@obulat You got it!
@donosco98 Assigned you jobs_test.JobRegistryTests :D
@DubeySandeep can I work on other parts of this issue? I have already completed one part.
@Hudda, which one would you like?
@kevinlee12 user_jobs_one_off_test.UserLastExplorationActivityOneOffJobTests will be fine.
Hi @Hudda, I've assigned you to `user_jobs_one_off_test.UserLastExplorationActivityOneOffJobTests`, you can go ahead and start working on it! :) 
Hi @rahmot, any updates on this? Please do let me know if you are stuck somewhere and I will be happy to help. Thanks!
Hi @KristiKovacs, any update on the assigned issue?
@DubeySandeep can I work on  models_test.BaseCalculationUnitTests .
Hi @donosco98,  I think you haven't filled the survey form yet, would you mind doing that first? Once you complete the instructions provided [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up), do let us know we will go ahead and assign you to the issue you have selected. :) 
@DubeySandeep , I've Filled the survey
Hello, I’m still working on it but I’ll need some help. I’ll email you
later today with some updates/questions.

On Thu, Oct 24, 2019 at 4:15 PM Sandeep Dubey <notifications@github.com>
wrote:

> Hi @KristiKovacs <https://github.com/KristiKovacs>, any update on the
> assigned issue?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/oppia/oppia/issues/7450?email_source=notifications&email_token=ALDNA7YV7SKECUPWEZINWGTQQH66LA5CNFSM4IPFH6PKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECGJKNA#issuecomment-546084148>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ALDNA73CVHTR3ROUBGHMN7TQQH66LANCNFSM4IPFH6PA>
> .
>

@donosco98, Thanks for filling the survey report, I've assigned you to `models_test.BaseCalculationUnitTests` so you can go ahead and start working on it! :) 
Hello, I'm an outreachy applicant and I would like to work on gae_models_test.GenerateHashTests.
@lorryaze, you got it!
Hello! I'd like to work on gae_models.VersionedModel

@tinumide, I've assigned you.
Regarding `draft_upgrade_services_test.DraftUpgradeUnitTests.test_try_upgrade_success`, it looks like the private method isn't being accessed. The method looks like this:

```python
    def test_try_upgrade_success(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(draft_upgrade_services.DraftUpgradeUtil.__dict__,
                "")
        draft_upgrade_services.DraftUpgradeUtil._convert_states_v0_dict_to_v1_dict = (
            classmethod(lambda cls, changelist: changelist))
        self.assertEqual(exploration.version, 2)
        self.assertEqual(
            draft_upgrade_services.try_upgrading_draft_to_exp_version(
                self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID),
            self.DRAFT_CHANGELIST)
```

The private method `_convert_states_v0_dict_to_v1_dict` is being created so that we can test `draft_upgrade_services.try_upgrading_draft_to_exp_version`, which relies on using a method of the name `_convert_states_v0_dict_to_v1_dict`. Is this acceptable usage of a private member, or should `draft_upgrade_services.try_upgrading_draft_to_exp_version` be changed to not use private methods?
`draft_upgrade_services.try_upgrading_draft_to_exp_version` should be changed to note use private methods. I would find out to see if you can perform the setup outside of the object instance. 
@U8NWXD One thing you could do is to swap out draft_upgrade_services.DraftUpgradeUtil with a MockDraftUpgradeUtil (for the DraftUpgradeUnitTests), and have that MockDraftUpgradeUtil define the appropriate conversion functions for the tests.
Hello everyone, I'm sorry for my lateness in response. I'd a little issue with my system. I am sorry for not informing you all about my absence. @ankita240796 thanks, I wouldn't hesitate to reach out to you whenever I encounter a problem. I'm starting over again. Glad to be here again! Thank you
I'm ready to take these three items:

user_services_test.LastLoginIntegrationTests
user_services_test.LastExplorationEditedIntegrationTests user_services_test.LastExplorationCreatedIntegrationTests
Thanks, @vpishuk! Assigned you. 
Created a PR: https://github.com/oppia/oppia/pull/7972
Hi! I would like to try working on html_validation_service_test.ContentMigrationTests
@cathuang20 you got it!
Hi! Can I work on jobs_test.BaseMapReduceJobManagerForContinuousComputationsTests?
@linden-li, you got it!
Hi, I want to try working on BaseContinuousComputationManagerTests. Can I get assigned? My plan is to modify the tests to have sub classes that call the private functions without implementing them and catching the error with regexp. Do you think I'm on the right track? Thanks
Hi! Can I please get assigned to skill_editor_test.BaseSkillEditorControllerTests?
@aunghoo you got it!
Hello! Could I work on extensions.interactions.base_test.InteractionUnitTests for my first PR? Thanks!
@aazuspan you got it!
Hey! I would like to work on: 
* `suggestion_services_test.SuggestionIntegrationTests`
* `suggestion_test.SuggestionUnitTests`
@ShivanshRakesh Ok, Assigned you. 
@kevinlee12  The issues:
* `editor_test.ExplorationEditorLogoutTest`
* `library_test.LibraryPageTests`

would also be fixed from the changes that I'll be doing for those in [my previous comment](https://github.com/oppia/oppia/issues/7450#issuecomment-566441591). Hence, pls assign these too.

@kevinlee12 `extensions.interactions.base_test.InteractionUnitTests` is completed and merged. Could I be assigned to  `exp_services.test` now? Thanks!
@aazuspan sure!
Hello! I would like to work on the collection_services_test. I am new to open source contribution and would request for your help for my first PR. Thanks!
@ShivanshRakesh I've assigned you
@sudiptapradhan1181 welcome! I've assigned you to `collection_services_test`. To get started, see [this wiki page](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up). In particular, make sure you fill out the CLA and contributor survey if you haven't already
Hello! Can I please be assigned to work on `user_query_jobs_one_off_test.UserQueryJobOneOffTests`?
@iamprayush Assigned you :+1:
Happy New Year everyone! I would like to try jobs.MapReduceJobPipeline.
Happy new year @Ariel-dev! I've assigned you :)
hey, i would like to work on  jobs_test.BaseContinuousComputationManagerTests.
I am new to open source and will need a little help.
Hey @kevinlee12 I would like to work on `user_jobs_one_off_test.UserFirstContributionMsecOneOffJobTests`. Please assign me. 
Thanks

@Arnesh07 you got it!
Hi! I would like to work on `rte_component_registry_test.RteComponentUnitTests`. Thanks!
@alexlee311 You got it!
Hello! Could I please work on `jobs_test.ContinuousComputationTests ` ?
@kiriakivel Go ahead! I've assigned you.
Hi @kevinlee12 , I would like to work on `draft_upgrade_services_test.DraftUpgradeUnitTests`
Hello! I would like to work on `email_manager.BulkEmailsTests ` please. Thanks!
Hey! Can I be assigned to email_manager_test.EmailRightsTest/test_sender_id_validation please? Thanks!
Hi @brianlinUM and @vijaykpatel, I've assigned you guys those files. Thanks!
@kevintab95 can I take-up editor_test.ExplorationEditorLogoutTest

@aggarwalpulkit596, done!
@kevintab95 there isn't a public function for this https://github.com/oppia/oppia/blob/0d5e3bf7f595377b65040f9f1dec73c179df79aa/core/domain/exp_services.py#L504 
so should i make this function public or make a public function for this
Hm, can you use `update_exploration`?
@kevinlee12 I'd like to take on  user_query_jobs_one_off_test.UserQueryJobOneOffTests
@wcascades you got it!
 I'm not sure of the best way to ask this question, so I'm sending this email. I've been over the "test_user_has_not_logged_in_last_n_days" a dozen times and I cannot see where an inappropriate private function is being called. I thought maybe I was just confused, but I looked at the other functions that were referenced in the issue ticket and in all the other methods, it was clear that a private function was inappropriately being used in the used in the function.
If you could look at this function and see if it actually has an issue, I would appreciate it. Again, I'm sorry if this isn't the preferred way for this question to be asked.
    On Monday, April 20, 2020, 12:48:22 PM PDT, mrmtheboss <notifications@github.com> wrote:  
 
 


as assisted by @Showtim3 , Can i work on this issue?


—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.  
@wcascades you are right, `setUp` doesn't have private functions, I've edited the list, but for the the other, see https://github.com/oppia/oppia/blob/25adfb75b8dc84096ecc6f71727037accb33b2c4/core/domain/user_query_jobs_one_off_test.py#L164
@kevinlee12 -- I think this one seems OK actually? The only problems are the ones where the line has `# pylint: disable=protected-access` in it.
Ah, yes. Let me take down that function then. @wcascades can you choose another file?
How about jobs_test.JobManagerUnitTests ?
sure!
@kevinlee12 can I please try 'draft_upgrade_services_test.DraftUpgradeUtilUnitTests'? 
@kaylahardie assigned you!
@kevinlee12 @seanlip Can I please be assigned ` jobs_test.JobRegistryTests` to work with? Thanks!
Done @sajalasati!

Resolving patch:
diff --git a/core/domain/exp_services.py b/core/domain/exp_services.py
--- a/core/domain/exp_services.py
+++ b/core/domain/exp_services.py
@@ -493,8 +493,18 @@ def apply_change_list(exploration_id, change_list):
                 # Exploration domain object automatically converts it to use
                 # the latest states schema version. As a result, simply
                 # resaving the exploration is sufficient to apply the states
-                # schema update.
-                continue
+                # schema update. Thus, no action is needed here other than
+                # to make sure that the version that the user is trying to
+                # migrate to is the latest version.
+                target_version_is_current_state_schema_version = (
+                    change.to_version ==
+                    python_utils.UNICODE(feconf.CURRENT_STATE_SCHEMA_VERSION))
+                if not target_version_is_current_state_schema_version:
+                    raise Exception(
+                        'Expected to migrate to the latest state schema '
+                        'version %s, received %s' % (
+                            feconf.CURRENT_STATE_SCHEMA_VERSION,
+                            change.to_version))
         return exploration
 
     except Exception as e:


Test patch:
diff --git a/core/domain/draft_upgrade_services_test.py b/core/domain/draft_upgrade_services_test.py
--- a/core/domain/draft_upgrade_services_test.py
+++ b/core/domain/draft_upgrade_services_test.py
@@ -25,6 +25,7 @@
 from core.domain import exp_services
 from core.tests import test_utils
 import feconf
+import python_utils
 import utils
 
 
@@ -40,7 +41,8 @@ class DraftUpgradeUnitTests(test_utils.GenericTestBase):
     EXP_MIGRATION_CHANGE_LIST = [exp_domain.ExplorationChange({
         'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
         'from_version': '0',
-        'to_version': '1'
+        'to_version': python_utils.UNICODE(
+            feconf.CURRENT_STATE_SCHEMA_VERSION)
     })]
     DRAFT_CHANGELIST = [exp_domain.ExplorationChange({
         'cmd': 'edit_exploration_property',
@@ -93,23 +95,62 @@ def test_try_upgrade_failure_due_to_unimplemented_upgrade_methods(self):
             draft_upgrade_services.try_upgrading_draft_to_exp_version(
                 self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID))
 
-    def test_try_upgrade_success(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)
-        draft_upgrade_services.DraftUpgradeUtil._convert_states_v0_dict_to_v1_dict = (  # pylint: disable=protected-access,line-too-long
-            classmethod(lambda cls, changelist: changelist))
-        self.assertEqual(exploration.version, 2)
-        self.assertEqual(
-            draft_upgrade_services.try_upgrading_draft_to_exp_version(
-                self.DRAFT_CHANGELIST, 1, exploration.version, self.EXP_ID),
-            self.DRAFT_CHANGELIST)
-
 
 class DraftUpgradeUtilUnitTests(test_utils.GenericTestBase):
     """Test the DraftUpgradeUtil module."""
 
+    # 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' % (
@@ -121,7 +162,7 @@ def test_convert_to_latest_schema_version_implemented(self):
             'unimplemented.' % (state_schema_version, conversion_fn_name))
 
     def test_convert_states_v32_dict_to_v33_dict(self):
-        draft_change_list = [
+        draft_change_list_v32 = [
             exp_domain.ExplorationChange({
                 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                 'state_name': 'state1',
@@ -157,15 +198,12 @@ def test_convert_states_v32_dict_to_v33_dict(self):
                         'value': 1
                     }
                 }
-            })]
-        expected_draft_change_list = (
-            draft_upgrade_services.DraftUpgradeUtil._convert_states_v32_dict_to_v33_dict(  # pylint: disable=protected-access,line-too-long
-                draft_change_list))
-        self.assertEqual(
-            expected_draft_change_list[1].to_dict(),
-            draft_change_list[1].to_dict())
-        self.assertEqual(
-            expected_draft_change_list[0].to_dict(),
+            })
+        ]
+        # 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',
@@ -183,23 +221,83 @@ def test_convert_states_v32_dict_to_v33_dict(self):
                         'value': False
                     }
                 }
-            }).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>',
+                            '<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 = [
+        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_upgrade_services.DraftUpgradeUtil._convert_states_v31_dict_to_v32_dict(  # pylint: disable=protected-access,line-too-long
-                draft_change_list)[0].to_dict(),
-            draft_change_list[0].to_dict())
+            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 = [
+        draft_change_list_v30 = [
             exp_domain.ExplorationChange({
                 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                 'state_name': 'Intro',
@@ -215,11 +313,10 @@ def test_convert_states_v30_dict_to_v31_dict(self):
                         }
                     }
                 }
-            })]
-        self.assertEqual(
-            draft_upgrade_services.DraftUpgradeUtil # pylint: disable=protected-access
-            ._convert_states_v30_dict_to_v31_dict(
-                draft_change_list)[0].to_dict(),
+            })
+        ]
+        # 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',
@@ -236,10 +333,30 @@ def test_convert_states_v30_dict_to_v31_dict(self):
                         }
                     }
                 }
-            }).to_dict())
+            })
+        ]
+        # 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 = [
+        draft_change_list_v29 = [
             exp_domain.ExplorationChange({
                 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
                 'property_name': 'answer_groups',
@@ -270,10 +387,11 @@ def test_convert_states_v29_dict_to_v30_dict(self):
                     'training_data': [],
                     'tagged_misconception_id': None
                 }
-            })]
-        self.assertEqual(
-            draft_upgrade_services.DraftUpgradeUtil._convert_states_v29_dict_to_v30_dict(  # pylint: disable=protected-access,line-too-long
-                draft_change_list)[0].to_dict(),
+            })
+        ]
+        # 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',
@@ -304,35 +422,94 @@ def test_convert_states_v29_dict_to_v30_dict(self):
                     'training_data': [],
                     'tagged_skill_misconception_id': None
                 }
-            }).to_dict())
+            })
+        ]
+        # 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 = [
+        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_upgrade_services.DraftUpgradeUtil._convert_states_v28_dict_to_v29_dict(  # pylint: disable=protected-access,line-too-long
-                draft_change_list)[0].to_dict(),
-            draft_change_list[0].to_dict())
+            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 = [
+        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',
-            })]
-        self.assertEqual(
-            draft_upgrade_services.DraftUpgradeUtil._convert_states_v27_dict_to_v28_dict(  # pylint: disable=protected-access,line-too-long
-                draft_change_list)[0].to_dict(),
+            })
+        ]
+        # 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'}
-            }).to_dict())
+            })
+        ]
+        # 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)
diff --git a/core/domain/exp_services_test.py b/core/domain/exp_services_test.py
--- a/core/domain/exp_services_test.py
+++ b/core/domain/exp_services_test.py
@@ -2638,6 +2638,49 @@ def test_update_written_translations_with_list_fails(self):
                     self.init_state_name, 'written_translations',
                     [1, 2]), 'Added fake text translations.')
 
+    def test_migrate_exp_to_latest_version_migrates_to_version(self):
+        """Test migrate exploration state schema to the latest version."""
+        latest_schema_version = python_utils.UNICODE(
+            feconf.CURRENT_STATE_SCHEMA_VERSION)
+        migration_change_list = [
+            exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
+                'from_version': '0',
+                'to_version': latest_schema_version
+            })
+        ]
+        exp_services.update_exploration(
+            self.owner_id, self.EXP_0_ID, migration_change_list,
+            'Ran Exploration Migration job.')
+        exploration = exp_fetchers.get_exploration_by_id(self.EXP_0_ID)
+        self.assertEqual(exploration.version, 2)
+        self.assertEqual(
+            python_utils.UNICODE(
+                exploration.states_schema_version),
+            latest_schema_version)
+
+    def test_migrate_exp_to_earlier_version_raises_exception(self):
+        """Test migrate state schema to earlier version raises exception."""
+        latest_schema_version = feconf.CURRENT_STATE_SCHEMA_VERSION
+        not_latest_schema_version = python_utils.UNICODE(
+            latest_schema_version - 1)
+        migration_change_list = [
+            exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
+                'from_version': '0',
+                'to_version': not_latest_schema_version
+            })
+        ]
+        exception_string = (
+            'Expected to migrate to the latest state schema '
+            'version %s, received %s' % (
+                latest_schema_version, not_latest_schema_version)
+        )
+        with self.assertRaisesRegexp(Exception, exception_string):
+            exp_services.update_exploration(
+                self.owner_id, self.EXP_0_ID, migration_change_list,
+                'Ran Exploration Migration job.')
+
 
 class CommitMessageHandlingTests(ExplorationServicesUnitTests):
     """Test the handling of commit messages."""