Repository: oppia/oppia

Issue:
Topic editor page with null as topic id throws 500
Topic editor page with null as topic id throws 500

**Steps to reproduce:**
1. Create an account and add the admin role to the user.
2. Go to `/topic_editor/null`

**Observed behavior:**
500 errors on the server.

**Expected behavior:**
It should through 400* error, not 500.

Topic editor page with null as topic id throws 500
Topic editor page with null as topic id throws 500

**Steps to reproduce:**
1. Create an account and add the admin role to the user.
2. Go to `/topic_editor/null`

**Observed behavior:**
500 errors on the server.

**Expected behavior:**
It should through 400* error, not 500.



Discussion:

Resolving patch:
diff --git a/core/controllers/acl_decorators.py b/core/controllers/acl_decorators.py
--- a/core/controllers/acl_decorators.py
+++ b/core/controllers/acl_decorators.py
@@ -1563,6 +1563,11 @@ def test_can_edit(self, topic_id, *args, **kwargs):
         if not self.user_id:
             raise base.UserFacingExceptions.NotLoggedInException
 
+        try:
+            topic_domain.Topic.require_valid_topic_id(topic_id)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
+
         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
         topic_rights = topic_services.get_topic_rights(topic_id, strict=False)
         if topic_rights is None or topic is None:
@@ -1773,6 +1778,11 @@ def test_can_add_story(self, topic_id, **kwargs):
         if not self.user_id:
             raise base.UserFacingExceptions.NotLoggedInException
 
+        try:
+            topic_domain.Topic.require_valid_topic_id(topic_id)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
+
         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
         topic_rights = topic_services.get_topic_rights(topic_id, strict=False)
         if topic_rights is None or topic is None:
@@ -2046,6 +2056,11 @@ def test_can_delete_topic(self, topic_id, **kwargs):
         if not self.user_id:
             raise self.NotLoggedInException
 
+        try:
+            topic_domain.Topic.require_valid_topic_id(topic_id)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
+
         user_actions_info = user_services.UserActionsInfo(self.user_id)
 
         if role_services.ACTION_DELETE_TOPIC in user_actions_info.actions:
@@ -2174,7 +2189,10 @@ def test_can_view_any_topic_editor(self, topic_id, **kwargs):
         """
         if not self.user_id:
             raise self.NotLoggedInException
-        topic_domain.Topic.require_valid_topic_id(topic_id)
+        try:
+            topic_domain.Topic.require_valid_topic_id(topic_id)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
 
         user_actions_info = user_services.UserActionsInfo(self.user_id)
 
@@ -2246,10 +2264,11 @@ def can_change_topic_publication_status(handler):
             if the user can publish or unpublish a topic.
     """
 
-    def test_can_change_topic_publication_status(self, **kwargs):
+    def test_can_change_topic_publication_status(self, topic_id, **kwargs):
         """Checks whether the user can can publish or unpublish a topic.
 
         Args:
+            topic_id: str. The topic id.
             **kwargs: *. Keyword arguments.
 
         Returns:
@@ -2263,12 +2282,17 @@ def test_can_change_topic_publication_status(self, **kwargs):
         if not self.user_id:
             raise self.NotLoggedInException
 
+        try:
+            topic_domain.Topic.require_valid_topic_id(topic_id)
+        except Exception as e:
+            raise self.PageNotFoundException(e)
+
         user_actions_info = user_services.UserActionsInfo(self.user_id)
 
         if (
                 role_services.ACTION_CHANGE_TOPIC_STATUS in
                 user_actions_info.actions):
-            return handler(self, **kwargs)
+            return handler(self, topic_id, **kwargs)
         else:
             raise self.UnauthorizedUserException(
                 '%s does not have enough rights to publish or unpublish the '
diff --git a/core/controllers/topic_editor.py b/core/controllers/topic_editor.py
--- a/core/controllers/topic_editor.py
+++ b/core/controllers/topic_editor.py
@@ -86,7 +86,6 @@ def post(self, topic_id):
         Currently, this only adds the story to the canonical story id list of
         the topic.
         """
-        topic_domain.Topic.require_valid_topic_id(topic_id)
         title = self.payload.get('title')
 
         story_domain.Story.require_valid_title(title)
@@ -217,7 +216,6 @@ def put(self, topic_id):
         subtopics), while False would mean it is for a Subtopic Page (this
         includes editing its html data as of now).
         """
-        topic_domain.Topic.require_valid_topic_id(topic_id)
         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
 
         version = self.payload.get('version')
@@ -275,7 +273,6 @@ def put(self, topic_id):
     @acl_decorators.can_delete_topic
     def delete(self, topic_id):
         """Handles Delete requests."""
-        topic_domain.Topic.require_valid_topic_id(topic_id)
         topic = topic_fetchers.get_topic_by_id(topic_id, strict=False)
         if topic is None:
             raise self.PageNotFoundException(
@@ -343,8 +340,6 @@ def put(self, topic_id):
         if topic is None:
             raise self.PageNotFoundException
 
-        topic_domain.Topic.require_valid_topic_id(topic_id)
-
         publish_status = self.payload.get('publish_status')
 
         if not isinstance(publish_status, bool):


Test patch:
diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py
--- a/core/controllers/acl_decorators_test.py
+++ b/core/controllers/acl_decorators_test.py
@@ -1859,6 +1859,7 @@ def setUp(self):
             [webapp2.Route('/mock_edit_topic/<topic_id>', self.MockHandler)],
             debug=feconf.DEBUG,
         ))
+        self.topic_id = topic_services.get_new_topic_id()
         self.save_new_topic(
             self.topic_id, self.viewer_id, name='Name',
             abbreviated_name='abbrev', thumbnail_filename=None,
@@ -2021,6 +2022,7 @@ def setUp(self):
                 '/mock_add_story_to_topic/<topic_id>', self.MockHandler)],
             debug=feconf.DEBUG,
         ))
+        self.topic_id = topic_services.get_new_topic_id()
         self.save_new_topic(
             self.topic_id, self.viewer_id, name='Name',
             abbreviated_name='abbrev', thumbnail_filename=None,
@@ -2047,6 +2049,15 @@ def test_admin_can_add_story_to_topic(self):
         self.assertEqual(response['topic_id'], self.topic_id)
         self.logout()
 
+    def test_topic_manager_cannot_add_story_to_topic_with_invalid_topic_id(
+            self):
+        self.login(self.manager_email)
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock_add_story_to_topic/incorrect_id',
+                expected_status_int=404)
+        self.logout()
+
     def test_topic_manager_can_add_story_to_topic(self):
         self.login(self.manager_email)
         with self.swap(self, 'testapp', self.mock_testapp):
@@ -2374,8 +2385,10 @@ class MockHandler(base.BaseHandler):
         GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
 
         @acl_decorators.can_change_topic_publication_status
-        def get(self):
-            self.render_json({})
+        def get(self, topic_id):
+            self.render_json({
+                topic_id: topic_id
+            })
 
     def setUp(self):
         super(ChangeTopicPublicationStatusTests, self).setUp()
@@ -2387,23 +2400,42 @@ def setUp(self):
         self.signup(self.banned_user_email, self.banned_user)
         self.set_banned_users([self.banned_user])
 
+        self.topic_id = topic_services.get_new_topic_id()
+        self.save_new_topic(
+            self.topic_id, self.admin_id, name='Name1',
+            abbreviated_name='abbrev', thumbnail_filename=None,
+            description='Description', canonical_story_ids=[],
+            additional_story_ids=[], uncategorized_skill_ids=[],
+            subtopics=[], next_subtopic_id=1)
+
         self.mock_testapp = webtest.TestApp(webapp2.WSGIApplication(
             [webapp2.Route(
-                '/mock_change_publication_status', self.MockHandler)],
+                '/mock_change_publication_status/<topic_id>',
+                self.MockHandler)],
             debug=feconf.DEBUG,
         ))
 
     def test_admin_can_change_topic_publication_status(self):
         self.login(self.ADMIN_EMAIL)
         with self.swap(self, 'testapp', self.mock_testapp):
-            self.get_json('/mock_change_publication_status')
+            self.get_json('/mock_change_publication_status/%s' % self.topic_id)
+        self.logout()
+
+    def test_can_not_change_topic_publication_status_with_invalid_topic_id(
+            self):
+        self.login(self.ADMIN_EMAIL)
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock_change_publication_status/invalid_topic_id',
+                expected_status_int=404)
         self.logout()
 
     def test_banned_user_cannot_change_topic_publication_status(self):
         self.login(self.banned_user_email)
         with self.swap(self, 'testapp', self.mock_testapp):
             response = self.get_json(
-                '/mock_change_publication_status', expected_status_int=401)
+                '/mock_change_publication_status/%s' % self.topic_id,
+                expected_status_int=401)
             self.assertIn(
                 'does not have enough rights to publish or unpublish the '
                 'topic.', response['error'])
@@ -2412,7 +2444,8 @@ def test_banned_user_cannot_change_topic_publication_status(self):
     def test_guest_cannot_change_topic_publication_status(self):
         with self.swap(self, 'testapp', self.mock_testapp):
             response = self.get_json(
-                '/mock_change_publication_status', expected_status_int=401)
+                '/mock_change_publication_status/%s' % self.topic_id,
+                expected_status_int=401)
         self.assertEqual(
             response['error'],
             'You must be logged in to access this resource.')
@@ -2832,6 +2865,16 @@ def test_can_edit_topic(self):
             self.assertEqual(response['entity_type'], 'topic')
         self.logout()
 
+    def test_cannot_edit_topic_with_invalid_topic_id(self):
+        self.login(self.ADMIN_EMAIL)
+        topic_id = 'incorrect_id'
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock_edit_entity/%s/%s' % (
+                    feconf.ENTITY_TYPE_TOPIC, topic_id),
+                expected_status_int=404)
+        self.logout()
+
     def test_can_edit_skill(self):
         self.login(self.ADMIN_EMAIL)
         skill_id = skill_services.get_new_skill_id()
diff --git a/core/controllers/topic_editor_test.py b/core/controllers/topic_editor_test.py
--- a/core/controllers/topic_editor_test.py
+++ b/core/controllers/topic_editor_test.py
@@ -252,7 +252,7 @@ def test_editable_subtopic_page_get(self):
 
 class TopicEditorTests(BaseTopicEditorControllerTests):
 
-    def test_get_can_not_access_topic_page_with_invalid_topic_id(self):
+    def test_get_can_not_access_topic_page_with_nonexistent_topic_id(self):
         self.login(self.ADMIN_EMAIL)
 
         self.get_html_response(
@@ -262,6 +262,16 @@ def test_get_can_not_access_topic_page_with_invalid_topic_id(self):
 
         self.logout()
 
+    def test_cannot_access_topic_editor_page_with_invalid_topic_id(self):
+        # Check that the editor page can not be accessed with an
+        # an invalid topic id.
+        self.login(self.NEW_USER_EMAIL)
+        self.get_html_response(
+            '%s/%s' % (
+                feconf.TOPIC_EDITOR_URL_PREFIX, 'invalid_topic_id'),
+            expected_status_int=404)
+        self.logout()
+
     def test_access_topic_editor_page(self):
         """Test access to editor pages for the sample topic."""
 
@@ -637,6 +647,15 @@ def test_guest_can_not_delete_topic(self):
             response['error'],
             'You must be logged in to access this resource.')
 
+    def test_cannot_delete_invalid_topic(self):
+        # Check that an invalid topic can not be deleted.
+        self.login(self.ADMIN_EMAIL)
+        self.delete_json(
+            '%s/%s' % (
+                feconf.TOPIC_EDITOR_DATA_URL_PREFIX,
+                'invalid_topic_id'), expected_status_int=404)
+        self.logout()
+
     def test_editable_topic_handler_delete(self):
         # Check that admins can delete a topic.
         self.login(self.ADMIN_EMAIL)