Repository: oppia/oppia

Issue:
[BUG]: Problems with image when translating lessons into Portugese
### Describe the bug

When reviewing some of the cards, reviewers can not see images.

### URL of the page where the issue is observed.

`/contributor-dashboard`

### Steps To Reproduce


1. Log in with the admin
2. Go to the admin page under the "Roles" tab, and give yourself the following roles:
    - Curriculum Admin
    - Translation Admin
3. Go to url /contributor-dashboard-admin and give yourself review translation rights in the Portugese language.
4. Go to creator dashboard. Create and publish an exploration that contains a content card with an image with accessibility text.
5. Go to topic and skills dashboard. Create a topic. Then, create a story in that topic. Create a chapter for that story and link the exploration to the chapter. Save story changes and finally publish the story.
6. Log in with a different account.
7. Go to the contributor dashboard page under the "Translate Text" tab, and submit a suggest in Portugese for the listed opportunity.
8. Log in with the original admin account.
9. Go to the contributor dashboard page under the "Review Translations" tab, and click on the opportunity listed. Finally, click on the listed suggestion. Observe that the image won't display and is replaced with the accessibility text.


### Expected Behavior

I expect images to load and display properly within the translation suggestion review modal.

### Screenshots/Videos

![Captura de tela 2023-10-26 122857](https://github.com/oppia/oppia/assets/41409951/67b5dea1-859c-46d0-8f11-e148069b8b22)


### What device are you using?

Desktop

### Operating System

_No response_

### What browsers are you seeing the problem on?

_No response_

### Browser version

_No response_

### Additional context

Debugging doc created by @StephenYu2018: https://docs.google.com/document/d/1sL74bmX1A7YLGrbLqY0yAuByZk3nCeIJl65V8sMmINI/edit?usp=sharing

### Tips for developers

Before tackling the bug, please use `git bisect` (see https://git-scm.com/docs/git-bisect) to investigate which PR caused it (you only need to go back as far as commit https://github.com/oppia/oppia/commits/9a334e9). If you find the PR, leave a comment on this issue pointing to it, or just say that it happened before commit 9a334e9 if you could reproduce it there. This will help us fix the issue by reverting the faulty PR.

Also, if this is your first issue, please make sure to follow https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#choosing-a-good-first-issue and https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up before claiming it. Thanks!

[BUG]: Problems with image when translating lessons into Portugese
### Describe the bug

When reviewing some of the cards, reviewers can not see images.

### URL of the page where the issue is observed.

`/contributor-dashboard`

### Steps To Reproduce


1. Log in with the admin
2. Go to the admin page under the "Roles" tab, and give yourself the following roles:
    - Curriculum Admin
    - Translation Admin
3. Go to url /contributor-dashboard-admin and give yourself review translation rights in the Portugese language.
4. Go to creator dashboard. Create and publish an exploration that contains a content card with an image with accessibility text.
5. Go to topic and skills dashboard. Create a topic. Then, create a story in that topic. Create a chapter for that story and link the exploration to the chapter. Save story changes and finally publish the story.
6. Log in with a different account.
7. Go to the contributor dashboard page under the "Translate Text" tab, and submit a suggest in Portugese for the listed opportunity.
8. Log in with the original admin account.
9. Go to the contributor dashboard page under the "Review Translations" tab, and click on the opportunity listed. Finally, click on the listed suggestion. Observe that the image won't display and is replaced with the accessibility text.


### Expected Behavior

I expect images to load and display properly within the translation suggestion review modal.

### Screenshots/Videos

![Captura de tela 2023-10-26 122857](https://github.com/oppia/oppia/assets/41409951/67b5dea1-859c-46d0-8f11-e148069b8b22)


### What device are you using?

Desktop

### Operating System

_No response_

### What browsers are you seeing the problem on?

_No response_

### Browser version

_No response_

### Additional context

Debugging doc created by @StephenYu2018: https://docs.google.com/document/d/1sL74bmX1A7YLGrbLqY0yAuByZk3nCeIJl65V8sMmINI/edit?usp=sharing

### Tips for developers

Before tackling the bug, please use `git bisect` (see https://git-scm.com/docs/git-bisect) to investigate which PR caused it (you only need to go back as far as commit https://github.com/oppia/oppia/commits/9a334e9). If you find the PR, leave a comment on this issue pointing to it, or just say that it happened before commit 9a334e9 if you could reproduce it there. This will help us fix the issue by reverting the faulty PR.

Also, if this is your first issue, please make sure to follow https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#choosing-a-good-first-issue and https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up before claiming it. Thanks!



Discussion:
hello @seanlip could you please assign this issue to me.

To solve the issue of images not displaying during the translation process in Portuguese lessons:

1.) Check image file paths and confirm their existence.
2.) Ensure that the image tags or code references are correctly linked to the images in the codebase.
@seanlip   can i work on this issue ?

@abansal15 @yash1378  Per the guidance at https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#choosing-a-good-first-issue, please provide an explanation of what your PR will do (with names of files you're changing, what you plan to change in each file, etc.). If it looks good, we can assign you to this issue.

Please also follow the other instructions on that wiki page if you have not yet done so. Thanks!
sure sir i will go through this guide once and will not disturb you unnecessarily from now onwards
Hello @chris7716 could please add `steps to reproduce` in the PR description, this issue seems interesting to me and i wanted to work.
Thank you
@pritam2317 Here are the steps to reproduce the issue on your local server:
1. Log in with the admin account.
2. Go to the admin page under the "Roles" tab, and give yourself the following roles:
  - Curriculum Admin
  - Translation Admin
3. Go to url `/contributor-dashboard-admin` and give yourself review translation rights in the Portugese language.
4. Go to creator dashboard. Create and publish an exploration that contains a content card with an image with accessibility text.
5. Go to topic and skills dashboard. Create a topic. Then, create a story in that topic. Create a chapter for that story and link the exploration to the chapter. Save story changes and finally publish the story.
6. Log in with a different account.
7. Go to the contributor dashboard page under the "Translate Text" tab, and submit a suggest in Portugese for the listed opportunity.
8. Log in with the original admin account.
9. Go to the contributor dashboard page under the "Review Translations" tab, and click on the opportunity listed. Finally, click on the listed suggestion. Observe that the image won't display and is replaced with the accessibility text.

@chris7716 Please add these steps to the issue description.
@pritam2317 @yash1378 @abansal15 I started a debugging doc for this issue [here](https://docs.google.com/document/d/1sL74bmX1A7YLGrbLqY0yAuByZk3nCeIJl65V8sMmINI/edit?usp=sharing). If you have discovered anything not mentioned in this document or in the issue thread, feel free to let me know!
@seanlip In regards to finding the earliest commit where this issue is reproducible, I have video proof that the issue existed as early as Dec 2022 when the Python version was last updated. The video shows where I got that commit from, what commit I'm checked out on during the manual test, and the occurrence of this issue.



https://github.com/oppia/oppia/assets/117241994/3962b0e9-a220-49a1-8781-b85e5b603508


Hey @StephenYu2018, I have left a comment in debugging doc, PTAL, Thanks! 
I have unassigned myself as I have finished investigating this issue for now. My entire debugging doc currently spans 30+ pages, so for time's sake, I recommend reading just the "Solution" section of my debugging doc to anyone who wants to implement my solution for this issue. If you have any comments, questions or concerns about the issue or my solution, please @ me in a comment under this thread and reassign me to this issue.
I re-assigned myself as we've decided in our team meeting that I'll implement the solution that I came up with.
hello @seanlip could you please assign this issue to me.

To solve the issue of images not displaying during the translation process in Portuguese lessons:

1.) Check image file paths and confirm their existence.
2.) Ensure that the image tags or code references are correctly linked to the images in the codebase.
@seanlip   can i work on this issue ?

@abansal15 @yash1378  Per the guidance at https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#choosing-a-good-first-issue, please provide an explanation of what your PR will do (with names of files you're changing, what you plan to change in each file, etc.). If it looks good, we can assign you to this issue.

Please also follow the other instructions on that wiki page if you have not yet done so. Thanks!
sure sir i will go through this guide once and will not disturb you unnecessarily from now onwards
Hello @chris7716 could please add `steps to reproduce` in the PR description, this issue seems interesting to me and i wanted to work.
Thank you
@pritam2317 Here are the steps to reproduce the issue on your local server:
1. Log in with the admin account.
2. Go to the admin page under the "Roles" tab, and give yourself the following roles:
  - Curriculum Admin
  - Translation Admin
3. Go to url `/contributor-dashboard-admin` and give yourself review translation rights in the Portugese language.
4. Go to creator dashboard. Create and publish an exploration that contains a content card with an image with accessibility text.
5. Go to topic and skills dashboard. Create a topic. Then, create a story in that topic. Create a chapter for that story and link the exploration to the chapter. Save story changes and finally publish the story.
6. Log in with a different account.
7. Go to the contributor dashboard page under the "Translate Text" tab, and submit a suggest in Portugese for the listed opportunity.
8. Log in with the original admin account.
9. Go to the contributor dashboard page under the "Review Translations" tab, and click on the opportunity listed. Finally, click on the listed suggestion. Observe that the image won't display and is replaced with the accessibility text.

@chris7716 Please add these steps to the issue description.
@pritam2317 @yash1378 @abansal15 I started a debugging doc for this issue [here](https://docs.google.com/document/d/1sL74bmX1A7YLGrbLqY0yAuByZk3nCeIJl65V8sMmINI/edit?usp=sharing). If you have discovered anything not mentioned in this document or in the issue thread, feel free to let me know!
@seanlip In regards to finding the earliest commit where this issue is reproducible, I have video proof that the issue existed as early as Dec 2022 when the Python version was last updated. The video shows where I got that commit from, what commit I'm checked out on during the manual test, and the occurrence of this issue.



https://github.com/oppia/oppia/assets/117241994/3962b0e9-a220-49a1-8781-b85e5b603508


Hey @StephenYu2018, I have left a comment in debugging doc, PTAL, Thanks! 
I have unassigned myself as I have finished investigating this issue for now. My entire debugging doc currently spans 30+ pages, so for time's sake, I recommend reading just the "Solution" section of my debugging doc to anyone who wants to implement my solution for this issue. If you have any comments, questions or concerns about the issue or my solution, please @ me in a comment under this thread and reassign me to this issue.
I re-assigned myself as we've decided in our team meeting that I'll implement the solution that I came up with.

Resolving patch:
diff --git a/core/controllers/suggestion.py b/core/controllers/suggestion.py
--- a/core/controllers/suggestion.py
+++ b/core/controllers/suggestion.py
@@ -264,17 +264,95 @@ def post(self) -> None:
         # adding an image, there is no method to remove the uploaded image.
         # See more - https://github.com/oppia/oppia/issues/14298
         if suggestion_type != feconf.SUGGESTION_TYPE_ADD_QUESTION:
+            assert isinstance(
+                suggestion,
+                suggestion_registry.SuggestionTranslateContent
+            )
+            self._copy_images_from_target_exploration_content_to_translation(
+                suggestion
+            )
+
             files = self.normalized_payload.get('files')
             new_image_filenames = (
                 suggestion.get_new_image_filenames_added_in_suggestion()
             )
             if new_image_filenames and files is not None:
-                _upload_suggestion_images(
-                    files, suggestion, new_image_filenames
+                new_image_files = {
+                    filename: image_blob
+                    for filename, image_blob
+                    in files.items()
+                    if filename in new_image_filenames
+                }
+                self._save_new_images_added_in_translation(
+                    new_image_files, suggestion
                 )
 
         self.render_json(self.values)
 
+    def _save_new_images_added_in_translation(
+        self,
+        new_files: Dict[str, str],
+        suggestion: suggestion_registry.SuggestionTranslateContent
+    ) -> None:
+        """Saves new images introduced in translation suggestion to storage.
+
+        Args:
+            new_files: dict. A mapping from each new image's filename to its
+                corresponding image blob.
+            suggestion: SuggestionTranslateContent. The translation suggestion
+                for which images are being uploaded.
+        """
+        for filename, image in new_files.items():
+            decoded_image = base64.decodebytes(image.encode('utf-8'))
+            file_format = (
+                image_validation_services.validate_image_and_filename(
+                    decoded_image, filename))
+            image_is_compressible = (
+                file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS)
+            fs_services.save_original_and_compressed_versions_of_image(
+                filename, suggestion.image_context, suggestion.target_id,
+                decoded_image, 'image', image_is_compressible)
+
+    def _copy_images_from_target_exploration_content_to_translation(
+        self,
+        suggestion: suggestion_registry.SuggestionTranslateContent
+    ) -> None:
+        """Creates copies of images from the suggestion's target exploration
+        for the translation suggestion to use.
+
+        Args:
+            suggestion: SuggestionTranslateContent. The translation suggestion
+                to copy its target exploration's images to.
+
+        Raises:
+            Exception. An image in the target exploration's content is not a
+                saved asset belonging to the target exploration.
+        """
+        target_image_filenames = (
+            html_cleaner.get_image_filenames_from_html_strings(
+                suggestion.get_target_entity_html_strings()
+            )
+        )
+        try:
+            fs_services.copy_images(
+                suggestion.target_type, suggestion.target_id,
+                suggestion.image_context, suggestion.target_id,
+                target_image_filenames
+            )
+        except ValueError as error:
+            _, source_asset_path, *_ = error.args
+            filename_start_index = source_asset_path.rfind('/') + 1
+            source_asset_filename = source_asset_path[filename_start_index:]
+            source_asset_directory = source_asset_path[:filename_start_index]
+            raise Exception(
+                'An image in the submitted translation\'s original content '
+                'named "%s" cannot be found. Please save it to the ' % (
+                    source_asset_filename
+                ) + 'backend file system at /%s ' % (
+                    source_asset_directory
+                ) + 'before submitting this translation again.'
+            ) from error
+
 
 class SuggestionToExplorationActionHandlerNormalizedPayloadDict(TypedDict):
     """Dict representation of SuggestionToExplorationActionHandler's
@@ -1278,43 +1356,3 @@ def _construct_exploration_suggestions(
         }
         suggestion_dicts.append(updated_suggestion_dict)
     return suggestion_dicts
-
-
-def _upload_suggestion_images(
-    files: Dict[str, str],
-    suggestion: suggestion_registry.BaseSuggestion,
-    filenames: List[str]
-) -> None:
-    """Saves a suggestion's images to storage.
-
-    Args:
-        files: dict. Files containing a mapping of image
-            filename to image blob.
-        suggestion: BaseSuggestion. The suggestion for which images are being
-            uploaded.
-        filenames: list(str). The image filenames.
-    """
-    suggestion_image_context = suggestion.image_context
-    # TODO(#10513): Find a way to save the images before the suggestion is
-    # created.
-    for filename in filenames:
-        image = files[filename]
-        decoded_image = base64.decodebytes(image.encode('utf-8'))
-        file_format = (
-            image_validation_services.validate_image_and_filename(
-                decoded_image, filename))
-        image_is_compressible = (
-            file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS)
-        fs_services.save_original_and_compressed_versions_of_image(
-            filename, suggestion_image_context, suggestion.target_id,
-            decoded_image, 'image', image_is_compressible)
-
-    target_entity_html_list = suggestion.get_target_entity_html_strings()
-    target_image_filenames = (
-        html_cleaner.get_image_filenames_from_html_strings(
-            target_entity_html_list))
-
-    fs_services.copy_images(
-        suggestion.target_type, suggestion.target_id,
-        suggestion_image_context, suggestion.target_id,
-        target_image_filenames)
diff --git a/core/platform/storage/cloud_storage_services.py b/core/platform/storage/cloud_storage_services.py
--- a/core/platform/storage/cloud_storage_services.py
+++ b/core/platform/storage/cloud_storage_services.py
@@ -122,8 +122,16 @@ def copy(
             folder.
         dest_assets_path: str. The path to the relevant file within the entity's
             assets folder.
+
+    Raises:
+        ValueError. Source asset does not exist at the given path.
     """
     src_blob = _get_bucket(bucket_name).get_blob(source_assets_path)
+    if src_blob is None:
+        raise ValueError(
+            'Source asset does not exist at %s.' % source_assets_path,
+            source_assets_path
+        )
     _get_bucket(bucket_name).copy_blob(
         src_blob, _get_bucket(bucket_name), new_name=dest_assets_path
     )
diff --git a/core/platform/storage/dev_mode_storage_services.py b/core/platform/storage/dev_mode_storage_services.py
--- a/core/platform/storage/dev_mode_storage_services.py
+++ b/core/platform/storage/dev_mode_storage_services.py
@@ -98,11 +98,14 @@ def copy(
             assets folder.
 
     Raises:
-        Exception. Source asset does not exist.
+        ValueError. Source asset does not exist at the given path.
     """
     src_blob = CLIENT.get_blob(source_assets_path)
     if src_blob is None:
-        raise Exception('Source asset does not exist.')
+        raise ValueError(
+            'Source asset does not exist at %s.' % source_assets_path,
+            source_assets_path
+        )
     CLIENT.copy_blob(src_blob, dest_assets_path)
 
 


Test patch:
diff --git a/core/controllers/suggestion_test.py b/core/controllers/suggestion_test.py
--- a/core/controllers/suggestion_test.py
+++ b/core/controllers/suggestion_test.py
@@ -454,7 +454,9 @@ def test_suggestion_to_exploration_handler_with_invalid_target_id(
 
         self.logout()
 
-    def test_owner_of_exploration_cannot_repond_to_own_suggestion(self) -> None:
+    def test_owner_of_exploration_cannot_respond_to_own_suggestion(
+        self
+    ) -> None:
         self.login(self.EDITOR_EMAIL)
 
         exp_id = 'new_exp_id'
@@ -827,35 +829,528 @@ def test_translation_accept_suggestion_by_reviewer(self) -> None:
         self.login(self.REVIEWER_EMAIL)
         csrf_token = self.get_new_csrf_token()
 
-        suggestion_to_accept = self.get_json(
-            '%s?author_id=%s' % (
-                feconf.SUGGESTION_LIST_URL_PREFIX,
-                self.translator_id))['suggestions'][0]
+        suggestion_to_accept = self.get_json(
+            '%s?author_id=%s' % (
+                feconf.SUGGESTION_LIST_URL_PREFIX,
+                self.translator_id))['suggestions'][0]
+
+        csrf_token = self.get_new_csrf_token()
+        with self.swap(
+            opportunity_services,
+            'update_translation_opportunity_with_accepted_suggestion',
+            lambda x, _: x
+        ):
+            self.put_json('%s/exploration/%s/%s' % (
+                feconf.SUGGESTION_ACTION_URL_PREFIX,
+                suggestion_to_accept['target_id'],
+                suggestion_to_accept['suggestion_id']), {
+                    'action': 'accept',
+                    'commit_message': 'commit message',
+                    'review_message': 'Accepted'
+                }, csrf_token=csrf_token)
+        suggestion_post_accept = self.get_json(
+            '%s?author_id=%s' % (
+                feconf.SUGGESTION_LIST_URL_PREFIX,
+                self.translator_id))['suggestions'][0]
+        self.assertEqual(
+            suggestion_post_accept['status'],
+            suggestion_models.STATUS_ACCEPTED)
+        self.logout()
+
+    def test_saves_new_images_uploaded_by_translation_submitter(
+        self
+    ) -> None:
+        exp_id = '12345678exp1'
+        exploration = (
+            self.save_new_linear_exp_with_state_names_and_interactions(
+                exp_id, self.editor_id, ['State 1'],
+                ['EndExploration'], category='Algebra'))
+
+        state_content_dict = {
+            'content_id': 'content_0',
+            'html': (
+                '<oppia-noninteractive-image filepath-with-value='
+                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
+                'alt-with-value="&quot;Image&quot;">'
+                '</oppia-noninteractive-image>')
+        }
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with utils.open_file(
+            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
+            'rb', encoding=None
+        ) as f:
+            raw_image = f.read()
+        self.post_json(
+            '%s/exploration/%s' % (
+                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
+            {'filename': 'img.png'},
+            csrf_token=csrf_token,
+            upload_files=[('image', 'unused_filename', raw_image)]
+        )
+        exp_services.update_exploration(
+            self.editor_id, exp_id, [exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
+                'state_name': 'State 1',
+                'new_value': state_content_dict
+            })], 'Changes content.')
+        rights_manager.publish_exploration(self.editor, exp_id)
+
+        story = story_domain.Story.create_default_story(
+            'story_123', 'A story', 'Description', self.TOPIC_ID, 'story-a')
+        story_services.save_new_story(self.owner_id, story)
+        topic_services.add_canonical_story(
+            self.owner_id, self.TOPIC_ID, 'story_123')
+        topic_services.publish_story(
+            self.TOPIC_ID, 'story_123', self.admin_id)
+
+        story_services.update_story(
+            self.owner_id, 'story_123', [story_domain.StoryChange({
+                'cmd': 'add_story_node',
+                'node_id': 'node_1',
+                'title': 'Node1',
+            }), story_domain.StoryChange({
+                'cmd': 'update_story_node_property',
+                'property_name': 'exploration_id',
+                'node_id': 'node_1',
+                'old_value': None,
+                'new_value': exp_id
+            })], 'Changes.')
+
+        exploration = exp_fetchers.get_exploration_by_id(exp_id)
+        text_to_translate = exploration.states['State 1'].content.html
+        self.logout()
+
+        self.login(self.TRANSLATOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
+                'suggestion_type': (
+                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+                'target_id': exp_id,
+                'target_version_at_submission': exploration.version,
+                'change_cmd': {
+                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                    'state_name': 'State 1',
+                    'content_id': 'content_0',
+                    'language_code': 'hi',
+                    'content_html': text_to_translate,
+                    'translation_html': (
+                        '<oppia-noninteractive-image filepath-with-value='
+                        '"&quot;translation_image.png&quot;" '
+                        'caption-with-value="&quot;&quot;" '
+                        'alt-with-value="&quot;Image&quot;">'
+                        '</oppia-noninteractive-image>'),
+                    'data_format': 'html'
+                },
+                'description': 'test',
+                'files': {
+                    'translation_image.png': (
+                        base64.b64encode(raw_image).decode('utf-8'))
+                 },
+            },
+            csrf_token=csrf_token
+        )
+
+        fs = fs_services.GcsFileSystem(
+            feconf.IMAGE_CONTEXT_EXPLORATION_SUGGESTIONS, exp_id)
+
+        self.assertTrue(fs.isfile('image/img.png'))
+        self.assertTrue(fs.isfile('image/img_compressed.png'))
+        self.assertTrue(fs.isfile('image/translation_image.png'))
+        self.assertTrue(fs.isfile('image/img_compressed.png'))
+
+    def test_copies_new_images_in_accepted_translation_to_target_exploration(
+        self
+    ) -> None:
+        exp_id = '12345678exp1'
+        exploration = (
+            self.save_new_linear_exp_with_state_names_and_interactions(
+                exp_id, self.editor_id, ['State 1'],
+                ['EndExploration'], category='Algebra'))
+
+        state_content_dict = {
+            'content_id': 'content_0',
+            'html': (
+                '<oppia-noninteractive-image filepath-with-value='
+                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
+                'alt-with-value="&quot;Image&quot;">'
+                '</oppia-noninteractive-image>')
+        }
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with utils.open_file(
+            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
+            'rb', encoding=None
+        ) as f:
+            raw_image = f.read()
+        self.post_json(
+            '%s/exploration/%s' % (
+                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
+            {'filename': 'img.png'},
+            csrf_token=csrf_token,
+            upload_files=[('image', 'unused_filename', raw_image)]
+        )
+        exp_services.update_exploration(
+            self.editor_id, exp_id, [exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
+                'state_name': 'State 1',
+                'new_value': state_content_dict
+            })], 'Changes content.')
+        rights_manager.publish_exploration(self.editor, exp_id)
+
+        story = story_domain.Story.create_default_story(
+            'story_123', 'A story', 'Description', self.TOPIC_ID, 'story-a')
+        story_services.save_new_story(self.owner_id, story)
+        topic_services.add_canonical_story(
+            self.owner_id, self.TOPIC_ID, 'story_123')
+        topic_services.publish_story(
+            self.TOPIC_ID, 'story_123', self.admin_id)
+
+        story_services.update_story(
+            self.owner_id, 'story_123', [story_domain.StoryChange({
+                'cmd': 'add_story_node',
+                'node_id': 'node_1',
+                'title': 'Node1',
+            }), story_domain.StoryChange({
+                'cmd': 'update_story_node_property',
+                'property_name': 'exploration_id',
+                'node_id': 'node_1',
+                'old_value': None,
+                'new_value': exp_id
+            })], 'Changes.')
+
+        exploration = exp_fetchers.get_exploration_by_id(exp_id)
+        text_to_translate = exploration.states['State 1'].content.html
+        self.logout()
+
+        self.login(self.TRANSLATOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
+                'suggestion_type': (
+                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+                'target_id': exp_id,
+                'target_version_at_submission': exploration.version,
+                'change_cmd': {
+                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                    'state_name': 'State 1',
+                    'content_id': 'content_0',
+                    'language_code': 'hi',
+                    'content_html': text_to_translate,
+                    'translation_html': (
+                        '<oppia-noninteractive-image filepath-with-value='
+                        '"&quot;translation_image.png&quot;" '
+                        'caption-with-value="&quot;&quot;" '
+                        'alt-with-value="&quot;Image&quot;">'
+                        '</oppia-noninteractive-image>'),
+                    'data_format': 'html'
+                },
+                'description': 'test',
+                'files': {
+                    'translation_image.png': (
+                        base64.b64encode(raw_image).decode('utf-8'))
+                 },
+            },
+            csrf_token=csrf_token
+        )
+        suggestion_to_accept = self.get_json(
+            '%s?author_id=%s' % (
+                feconf.SUGGESTION_LIST_URL_PREFIX,
+                self.translator_id))['suggestions'][0]
+        self.logout()
+
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with self.swap(
+            opportunity_services,
+            'update_translation_opportunity_with_accepted_suggestion',
+            lambda x, _: x
+        ):
+            self.put_json('%s/exploration/%s/%s' % (
+                feconf.SUGGESTION_ACTION_URL_PREFIX,
+                suggestion_to_accept['target_id'],
+                suggestion_to_accept['suggestion_id']), {
+                    'action': 'accept',
+                    'commit_message': 'Translated content of State 1',
+                    'review_message': 'This looks good!',
+                }, csrf_token=csrf_token)
+
+        fs = fs_services.GcsFileSystem(feconf.ENTITY_TYPE_EXPLORATION, exp_id)
+        self.assertTrue(fs.isfile('image/img.png'))
+        self.assertTrue(fs.isfile('image/translation_image.png'))
+        self.assertTrue(fs.isfile('image/img_compressed.png'))
+
+    def test_raises_exception_if_new_contentless_image_in_translation(
+        self
+    ) -> None:
+        exp_id = '12345678exp1'
+        exploration = (
+            self.save_new_linear_exp_with_state_names_and_interactions(
+                exp_id, self.editor_id, ['State 1'],
+                ['EndExploration'], category='Algebra'))
+
+        state_content_dict = {
+            'content_id': 'content_0',
+            'html': (
+                '<oppia-noninteractive-image filepath-with-value='
+                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
+                'alt-with-value="&quot;Image&quot;">'
+                '</oppia-noninteractive-image>')
+        }
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with utils.open_file(
+            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
+            'rb', encoding=None
+        ) as f:
+            raw_image = f.read()
+        self.post_json(
+            '%s/exploration/%s' % (
+                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
+            {'filename': 'img.png'},
+            csrf_token=csrf_token,
+            upload_files=[('image', 'unused_filename', raw_image)])
+        exp_services.update_exploration(
+            self.editor_id, exp_id, [exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
+                'state_name': 'State 1',
+                'new_value': state_content_dict
+            })], 'Changes content.')
+        rights_manager.publish_exploration(self.editor, exp_id)
+
+        exploration = exp_fetchers.get_exploration_by_id(exp_id)
+        text_to_translate = exploration.states['State 1'].content.html
+        self.logout()
+
+        valid_html = (
+            '<oppia-noninteractive-math math_content-with-value="{&amp;q'
+            'uot;raw_latex&amp;quot;: &amp;quot;(x - a_1)(x - a_2)(x - a'
+            '_3)...(x - a_n-1)(x - a_n)&amp;quot;, &amp;quot;svg_filenam'
+            'e&amp;quot;: &amp;quot;file.svg&amp;quot;}"></oppia-noninte'
+            'ractive-math>'
+        )
+        self.login(self.TRANSLATOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+        response_dict = self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
+                'suggestion_type': (
+                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+                'target_id': exp_id,
+                'target_version_at_submission': exploration.version,
+                'change_cmd': {
+                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                    'state_name': 'State 1',
+                    'content_id': 'content_0',
+                    'language_code': 'hi',
+                    'content_html': text_to_translate,
+                    'translation_html': valid_html,
+                    'data_format': 'html'
+                },
+                'files': {'file.svg': None},
+                'description': 'test'
+            }, csrf_token=csrf_token, expected_status_int=400)
+
+        self.assertIn('No image supplied', response_dict['error'])
+        self.logout()
+
+    def test_raises_exception_if_new_image_in_translation_exceeds_file_size_limit( # pylint: disable=line-too-long
+        self
+    ) -> None:
+        exp_id = '12345678exp1'
+        exploration = (
+            self.save_new_linear_exp_with_state_names_and_interactions(
+                exp_id, self.editor_id, ['State 1'],
+                ['EndExploration'], category='Algebra'))
+
+        state_content_dict = {
+            'content_id': 'content_0',
+            'html': (
+                '<oppia-noninteractive-image filepath-with-value='
+                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
+                'alt-with-value="&quot;Image&quot;">'
+                '</oppia-noninteractive-image>')
+        }
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with utils.open_file(
+            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
+            'rb', encoding=None
+        ) as f:
+            raw_image = f.read()
+        self.post_json(
+            '%s/exploration/%s' % (
+                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
+            {'filename': 'img.png'},
+            csrf_token=csrf_token,
+            upload_files=[('image', 'unused_filename', raw_image)])
+        exp_services.update_exploration(
+            self.editor_id, exp_id, [exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
+                'state_name': 'State 1',
+                'new_value': state_content_dict
+            })], 'Changes content.')
+        rights_manager.publish_exploration(self.editor, exp_id)
+
+        exploration = exp_fetchers.get_exploration_by_id(exp_id)
+        text_to_translate = exploration.states['State 1'].content.html
+        self.logout()
+
+        valid_html = (
+            '<oppia-noninteractive-math math_content-with-value="{&amp;q'
+            'uot;raw_latex&amp;quot;: &amp;quot;(x - a_1)(x - a_2)(x - a'
+            '_3)...(x - a_n-1)(x - a_n)&amp;quot;, &amp;quot;svg_filenam'
+            'e&amp;quot;: &amp;quot;file.svg&amp;quot;}"></oppia-noninte'
+            'ractive-math>'
+        )
+        large_image = '<svg><path d="%s" /></svg>' % (
+             'M150 0 L75 200 L225 200 Z ' * 4000)
+        self.login(self.TRANSLATOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+        response_dict = self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
+                'suggestion_type': (
+                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+                'target_id': exp_id,
+                'target_version_at_submission': exploration.version,
+                'change_cmd': {
+                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                    'state_name': 'State 1',
+                    'content_id': 'content_0',
+                    'language_code': 'hi',
+                    'content_html': text_to_translate,
+                    'translation_html': valid_html,
+                    'data_format': 'html'
+                },
+                'description': 'test',
+                'files': {'file.svg': large_image},
+            }, csrf_token=csrf_token, expected_status_int=400,
+        )
+
+        self.assertIn(
+            'Image exceeds file size limit of 100 KB.',
+            response_dict['error'])
+        self.logout()
+
+    def test_saves_new_images_in_translation_that_were_copied_from_target_exploration( # pylint: disable=line-too-long
+        self
+    ) -> None:
+        exp_id = '12345678exp1'
+        exploration = (
+            self.save_new_linear_exp_with_state_names_and_interactions(
+                exp_id, self.editor_id, ['State 1'],
+                ['EndExploration'], category='Algebra'))
+
+        state_content_dict = {
+            'content_id': 'content_0',
+            'html': (
+                '<oppia-noninteractive-image filepath-with-value='
+                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
+                'alt-with-value="&quot;Image&quot;">'
+                '</oppia-noninteractive-image>')
+        }
+        self.login(self.EDITOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        with utils.open_file(
+            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
+            'rb', encoding=None
+        ) as f:
+            raw_image = f.read()
+        self.post_json(
+            '%s/exploration/%s' % (
+                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
+            {'filename': 'img.png'},
+            csrf_token=csrf_token,
+            upload_files=[('image', 'unused_filename', raw_image)]
+        )
+        exp_services.update_exploration(
+            self.editor_id, exp_id, [exp_domain.ExplorationChange({
+                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
+                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
+                'state_name': 'State 1',
+                'new_value': state_content_dict
+            })], 'Changes content.')
+        rights_manager.publish_exploration(self.editor, exp_id)
+
+        story = story_domain.Story.create_default_story(
+            'story_123', 'A story', 'Description', self.TOPIC_ID, 'story-a')
+        story_services.save_new_story(self.owner_id, story)
+        topic_services.add_canonical_story(
+            self.owner_id, self.TOPIC_ID, 'story_123')
+        topic_services.publish_story(
+            self.TOPIC_ID, 'story_123', self.admin_id)
+
+        story_services.update_story(
+            self.owner_id, 'story_123', [story_domain.StoryChange({
+                'cmd': 'add_story_node',
+                'node_id': 'node_1',
+                'title': 'Node1',
+            }), story_domain.StoryChange({
+                'cmd': 'update_story_node_property',
+                'property_name': 'exploration_id',
+                'node_id': 'node_1',
+                'old_value': None,
+                'new_value': exp_id
+            })], 'Changes.')
+
+        exploration = exp_fetchers.get_exploration_by_id(exp_id)
+        text_to_translate = exploration.states['State 1'].content.html
+        self.logout()
+
+        self.login(self.TRANSLATOR_EMAIL)
+        csrf_token = self.get_new_csrf_token()
+
+        self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
+                'suggestion_type': (
+                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+                'target_id': exp_id,
+                'target_version_at_submission': exploration.version,
+                'change_cmd': {
+                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                    'state_name': 'State 1',
+                    'content_id': 'content_0',
+                    'language_code': 'hi',
+                    'content_html': text_to_translate,
+                    'translation_html': (
+                        '<oppia-noninteractive-image filepath-with-value='
+                        '"&quot;img.png&quot;" '
+                        'caption-with-value="&quot;&quot;" '
+                        'alt-with-value="&quot;Image&quot;">'
+                        '</oppia-noninteractive-image>'),
+                    'data_format': 'html'
+                },
+                'description': 'test',
+                'files': {},
+            },
+            csrf_token=csrf_token
+        )
+
+        fs = fs_services.GcsFileSystem(
+            feconf.IMAGE_CONTEXT_EXPLORATION_SUGGESTIONS, exp_id)
 
-        csrf_token = self.get_new_csrf_token()
-        with self.swap(
-            opportunity_services,
-            'update_translation_opportunity_with_accepted_suggestion',
-            lambda x, _: x
-        ):
-            self.put_json('%s/exploration/%s/%s' % (
-                feconf.SUGGESTION_ACTION_URL_PREFIX,
-                suggestion_to_accept['target_id'],
-                suggestion_to_accept['suggestion_id']), {
-                    'action': 'accept',
-                    'commit_message': 'commit message',
-                    'review_message': 'Accepted'
-                }, csrf_token=csrf_token)
-        suggestion_post_accept = self.get_json(
-            '%s?author_id=%s' % (
-                feconf.SUGGESTION_LIST_URL_PREFIX,
-                self.translator_id))['suggestions'][0]
-        self.assertEqual(
-            suggestion_post_accept['status'],
-            suggestion_models.STATUS_ACCEPTED)
-        self.logout()
+        self.assertTrue(fs.isfile('image/img.png'))
 
-    def test_translation_suggestion_creation_with_new_images(self) -> None:
+    def test_raises_exception_if_target_exploration_image_is_not_in_target_exploration_assets( # pylint: disable=line-too-long
+        self
+    ) -> None:
         exp_id = '12345678exp1'
         exploration = (
             self.save_new_linear_exp_with_state_names_and_interactions(
@@ -919,78 +1414,50 @@ def test_translation_suggestion_creation_with_new_images(self) -> None:
         text_to_translate = exploration.states['State 1'].content.html
         self.logout()
 
-        fs = fs_services.GcsFileSystem(feconf.ENTITY_TYPE_EXPLORATION, exp_id)
-
-        self.assertTrue(fs.isfile('image/img.png'))
+        fs = fs_services.GcsFileSystem(
+            feconf.ENTITY_TYPE_EXPLORATION, exp_id
+        )
+        fs.delete('image/img.png')
 
         self.login(self.TRANSLATOR_EMAIL)
         csrf_token = self.get_new_csrf_token()
-
-        self.post_json(
-            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
-                'suggestion_type': (
-                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
-                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
-                'target_id': exp_id,
-                'target_version_at_submission': exploration.version,
-                'change_cmd': {
-                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
-                    'state_name': 'State 1',
-                    'content_id': 'content_0',
-                    'language_code': 'hi',
-                    'content_html': text_to_translate,
-                    'translation_html': (
-                        '<oppia-noninteractive-image filepath-with-value='
-                        '"&quot;translation_image.png&quot;" '
-                        'caption-with-value="&quot;&quot;" '
-                        'alt-with-value="&quot;Image&quot;">'
-                        '</oppia-noninteractive-image>'),
-                    'data_format': 'html'
-                },
-                'description': 'test',
-                'files': {
-                    'translation_image.png': (
-                        base64.b64encode(raw_image).decode('utf-8'))
-                 },
+        request_body = {
+            'suggestion_type': (
+                feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
+            'target_type': feconf.ENTITY_TYPE_EXPLORATION,
+            'target_id': exp_id,
+            'target_version_at_submission': exploration.version,
+            'change_cmd': {
+                'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
+                'state_name': 'State 1',
+                'content_id': 'content_0',
+                'language_code': 'hi',
+                'content_html': text_to_translate,
+                'translation_html': (
+                    '<oppia-noninteractive-image filepath-with-value='
+                    '"&quot;img.png&quot;" '
+                    'caption-with-value="&quot;&quot;" '
+                    'alt-with-value="&quot;Image&quot;">'
+                    '</oppia-noninteractive-image>'),
+                'data_format': 'html'
             },
-            csrf_token=csrf_token
-        )
-
-        fs = fs_services.GcsFileSystem(
-            feconf.IMAGE_CONTEXT_EXPLORATION_SUGGESTIONS, exp_id)
-
-        self.assertTrue(fs.isfile('image/img.png'))
-        self.assertTrue(fs.isfile('image/img_compressed.png'))
-        self.assertTrue(fs.isfile('image/translation_image.png'))
-        self.assertTrue(fs.isfile('image/img_compressed.png'))
-
-        suggestion_to_accept = self.get_json(
-            '%s?author_id=%s' % (
-                feconf.SUGGESTION_LIST_URL_PREFIX,
-                self.translator_id))['suggestions'][0]
-        self.logout()
-
-        self.login(self.EDITOR_EMAIL)
-        csrf_token = self.get_new_csrf_token()
+            'description': 'test',
+            'files': {},
+        }
 
-        with self.swap(
-            opportunity_services,
-            'update_translation_opportunity_with_accepted_suggestion',
-            lambda x, _: x
-        ):
-            self.put_json('%s/exploration/%s/%s' % (
-                feconf.SUGGESTION_ACTION_URL_PREFIX,
-                suggestion_to_accept['target_id'],
-                suggestion_to_accept['suggestion_id']), {
-                    'action': 'accept',
-                    'commit_message': 'Translated content of State 1',
-                    'review_message': 'This looks good!',
-                }, csrf_token=csrf_token)
+        response_dict = self.post_json(
+            '%s/' % feconf.SUGGESTION_URL_PREFIX, request_body,
+            csrf_token=csrf_token, expected_status_int=500
+        )
 
-        fs = fs_services.GcsFileSystem(feconf.ENTITY_TYPE_EXPLORATION, exp_id)
-        self.assertTrue(fs.isfile('image/img.png'))
-        self.assertTrue(fs.isfile('image/translation_image.png'))
-        self.assertTrue(fs.isfile('image/img_compressed.png'))
+        self.assertIn(
+            'An image in the submitted translation\'s original content '
+            'named "img.png" cannot be found. Please save it to the '
+            'backend file system at /exploration/%s/assets/image/ ' % (
+                exp_id
+            ) + 'before submitting this translation again.',
+            response_dict['error']
+        )
 
     def test_set_of_strings_translation_suggestion_creation(self) -> None:
         self.login(self.TRANSLATOR_EMAIL)
@@ -1681,159 +2148,6 @@ def test_cannot_update_question_when_provided_state_data_is_invalid(
         )
         self.logout()
 
-    def test_suggestion_creation_when_images_are_not_provided(self) -> None:
-        exp_id = '12345678exp1'
-        exploration = (
-            self.save_new_linear_exp_with_state_names_and_interactions(
-                exp_id, self.editor_id, ['State 1'],
-                ['EndExploration'], category='Algebra'))
-
-        state_content_dict = {
-            'content_id': 'content_0',
-            'html': (
-                '<oppia-noninteractive-image filepath-with-value='
-                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
-                'alt-with-value="&quot;Image&quot;">'
-                '</oppia-noninteractive-image>')
-        }
-        self.login(self.EDITOR_EMAIL)
-        csrf_token = self.get_new_csrf_token()
-
-        with utils.open_file(
-            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
-            'rb', encoding=None
-        ) as f:
-            raw_image = f.read()
-        self.post_json(
-            '%s/exploration/%s' % (
-                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
-            {'filename': 'img.png'},
-            csrf_token=csrf_token,
-            upload_files=[('image', 'unused_filename', raw_image)])
-        exp_services.update_exploration(
-            self.editor_id, exp_id, [exp_domain.ExplorationChange({
-                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
-                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
-                'state_name': 'State 1',
-                'new_value': state_content_dict
-            })], 'Changes content.')
-        rights_manager.publish_exploration(self.editor, exp_id)
-
-        exploration = exp_fetchers.get_exploration_by_id(exp_id)
-        text_to_translate = exploration.states['State 1'].content.html
-        self.logout()
-
-        valid_html = (
-            '<oppia-noninteractive-math math_content-with-value="{&amp;q'
-            'uot;raw_latex&amp;quot;: &amp;quot;(x - a_1)(x - a_2)(x - a'
-            '_3)...(x - a_n-1)(x - a_n)&amp;quot;, &amp;quot;svg_filenam'
-            'e&amp;quot;: &amp;quot;file.svg&amp;quot;}"></oppia-noninte'
-            'ractive-math>'
-        )
-        self.login(self.TRANSLATOR_EMAIL)
-        csrf_token = self.get_new_csrf_token()
-        response_dict = self.post_json(
-            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
-                'suggestion_type': (
-                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
-                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
-                'target_id': exp_id,
-                'target_version_at_submission': exploration.version,
-                'change_cmd': {
-                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
-                    'state_name': 'State 1',
-                    'content_id': 'content_0',
-                    'language_code': 'hi',
-                    'content_html': text_to_translate,
-                    'translation_html': valid_html,
-                    'data_format': 'html'
-                },
-                'files': {'file.svg': None},
-                'description': 'test'
-            }, csrf_token=csrf_token, expected_status_int=400)
-
-        self.assertIn('No image supplied', response_dict['error'])
-        self.logout()
-
-    def test_suggestion_creation_when_images_are_not_valid(self) -> None:
-        exp_id = '12345678exp1'
-        exploration = (
-            self.save_new_linear_exp_with_state_names_and_interactions(
-                exp_id, self.editor_id, ['State 1'],
-                ['EndExploration'], category='Algebra'))
-
-        state_content_dict = {
-            'content_id': 'content_0',
-            'html': (
-                '<oppia-noninteractive-image filepath-with-value='
-                '"&quot;img.png&quot;" caption-with-value="&quot;&quot;" '
-                'alt-with-value="&quot;Image&quot;">'
-                '</oppia-noninteractive-image>')
-        }
-        self.login(self.EDITOR_EMAIL)
-        csrf_token = self.get_new_csrf_token()
-
-        with utils.open_file(
-            os.path.join(feconf.TESTS_DATA_DIR, 'img.png'),
-            'rb', encoding=None
-        ) as f:
-            raw_image = f.read()
-        self.post_json(
-            '%s/exploration/%s' % (
-                feconf.EXPLORATION_IMAGE_UPLOAD_PREFIX, exp_id),
-            {'filename': 'img.png'},
-            csrf_token=csrf_token,
-            upload_files=[('image', 'unused_filename', raw_image)])
-        exp_services.update_exploration(
-            self.editor_id, exp_id, [exp_domain.ExplorationChange({
-                'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
-                'property_name': exp_domain.STATE_PROPERTY_CONTENT,
-                'state_name': 'State 1',
-                'new_value': state_content_dict
-            })], 'Changes content.')
-        rights_manager.publish_exploration(self.editor, exp_id)
-
-        exploration = exp_fetchers.get_exploration_by_id(exp_id)
-        text_to_translate = exploration.states['State 1'].content.html
-        self.logout()
-
-        valid_html = (
-            '<oppia-noninteractive-math math_content-with-value="{&amp;q'
-            'uot;raw_latex&amp;quot;: &amp;quot;(x - a_1)(x - a_2)(x - a'
-            '_3)...(x - a_n-1)(x - a_n)&amp;quot;, &amp;quot;svg_filenam'
-            'e&amp;quot;: &amp;quot;file.svg&amp;quot;}"></oppia-noninte'
-            'ractive-math>'
-        )
-        large_image = '<svg><path d="%s" /></svg>' % (
-             'M150 0 L75 200 L225 200 Z ' * 4000)
-        self.login(self.TRANSLATOR_EMAIL)
-        csrf_token = self.get_new_csrf_token()
-        response_dict = self.post_json(
-            '%s/' % feconf.SUGGESTION_URL_PREFIX, {
-                'suggestion_type': (
-                    feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT),
-                'target_type': feconf.ENTITY_TYPE_EXPLORATION,
-                'target_id': exp_id,
-                'target_version_at_submission': exploration.version,
-                'change_cmd': {
-                    'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION,
-                    'state_name': 'State 1',
-                    'content_id': 'content_0',
-                    'language_code': 'hi',
-                    'content_html': text_to_translate,
-                    'translation_html': valid_html,
-                    'data_format': 'html'
-                },
-                'description': 'test',
-                'files': {'file.svg': large_image},
-            }, csrf_token=csrf_token, expected_status_int=400,
-        )
-
-        self.assertIn(
-            'Image exceeds file size limit of 100 KB.',
-            response_dict['error'])
-        self.logout()
-
 
 class QuestionSuggestionTests(test_utils.GenericTestBase):
 
diff --git a/core/platform/storage/cloud_storage_services_test.py b/core/platform/storage/cloud_storage_services_test.py
--- a/core/platform/storage/cloud_storage_services_test.py
+++ b/core/platform/storage/cloud_storage_services_test.py
@@ -254,6 +254,19 @@ def test_copy_creates_copy_in_the_bucket(self) -> None:
             'image/png'
         )
 
+    def test_raises_value_error_when_copying_from_non_existent_file(
+        self
+    ) -> None:
+        non_existent_source_path = 'path/to/file.txt'
+
+        with self.get_bucket_swap, self.assertRaisesRegex(
+            ValueError,
+            'Source asset does not exist at %s.' % non_existent_source_path
+        ):
+            cloud_storage_services.copy(
+                'bucket_1', non_existent_source_path, 'other/path/to/file.txt'
+            )
+
     def test_listdir_lists_files_with_provided_prefix(self) -> None:
         self.bucket_1.blobs['path/to/file.txt'] = MockBlob('path/to/file.txt')
         self.bucket_1.blobs['path/to/file.txt'].upload_from_string(b'abc')
diff --git a/core/platform/storage/dev_mode_storage_services_test.py b/core/platform/storage/dev_mode_storage_services_test.py
--- a/core/platform/storage/dev_mode_storage_services_test.py
+++ b/core/platform/storage/dev_mode_storage_services_test.py
@@ -73,7 +73,7 @@ def test_copy_with_existing_source_blob_is_successful(self) -> None:
 
     def test_copy_with_non_existing_source_blob_fails(self) -> None:
         with self.assertRaisesRegex(
-                Exception, 'Source asset does not exist'
+            ValueError, 'Source asset does not exist at /file/path.png'
         ):
             dev_mode_storage_services.copy(
                 'bucket', '/file/path.png', '/copy/path.png')