/
githubmirror
/
oppia
Обзор
Документация
Войти
/
githubmirror
/
oppia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
core/domain/html_validation_service_test.py
1 631 строка
76 KB
Sean Lip
Remove dependencies from common.py and from scripts on core (#23993)
09 дек 2025, 20:34
Не верифицирован
09 дек 2025, 20:34
2c53071
Код
Авторство
О чём код?
# coding: utf-8 # # Copyright 2018 The Oppia Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS-IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for the HTML validation.""" from __future__ import annotations import os import re from core import feconf from core.domain import fs_services, html_validation_service from core.tests import test_utils import bs4 from typing import Dict, List, TypedDict class SvgDiagramTestCaseDict(TypedDict): """Dict representing the test case SVG content Dictionary.""" html_content: str expected_output: bool class ContentMigrationTests(test_utils.GenericTestBase): """Tests the function associated with the migration of html strings to valid RTE format. """ def test_wrap_with_siblings(self) -> None: test_cases = [ { 'html_content': ( '<p><i>hello</i></p> this is<i>test case1</i> for ' '<ol><li><i>testing</i></li></ol>' ), 'expected_output': ( '<p><i>hello</i></p><p> this is<i>test case1</i> for </p>' '<ol><li><i>testing</i></li></ol>' ), }, { 'html_content': ( '<br/>hello this is<br/>test<p> case2<br/>' '</p> for <p><br/>testing</p>' ), 'expected_output': ( '<p><br/>hello this is<br/>test</p>' '<p> case2<br/></p> for <p><br/>testing</p>' ), }, { 'html_content': ( '<p>hello</p>this is case <b>3</b> for <i>' 'testing</i> the <p>function</p>' ), 'expected_output': ( '<p>hello</p><p>this is case <b>3</b> for <i>' 'testing</i> the </p><p>function</p>' ), }, ] for index, test_case in enumerate(test_cases): soup = bs4.BeautifulSoup(test_case['html_content'], 'html.parser') if index == 0: tag = soup.findAll(name='i')[1] elif index == 1: tag = soup.find(name='br') elif index == 2: tag = soup.find(name='b') html_validation_service.wrap_with_siblings(tag, soup.new_tag('p')) self.assertEqual(str(soup), test_case['expected_output']) def test_validate_rte_format(self) -> None: test_cases_for_ckeditor = [ ( '<pre>Hello this is <b> testing ' '<oppia-noninteractive-image filepath-with-value="amp;quot;' 'random.png&quot;"></oppia-noninteractive-image> in ' '</b>progress</pre>' ), ( '<oppia-noninteractive-collapsible content-with-value="&' 'quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" heading-with-value="&quot;' 'lorem ipsum&quot;lorem ipsum&quot;?&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value' '=\"[{&quot;content&quot;:&quot;&lt;p&gt;' '&lt;i&gt;lorem ipsum&lt;/i&gt;&lt;/p' '&gt;&quot;,&quot;title&quot;:&' 'quot;hello&quot;}]\"></oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-collapsible ' 'heading-with-value="&quot;' 'lorem ipsum&quot;lorem ipsum&quot;?&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-workedexample question-with-value="&' 'quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" answer-with-value="&quot;' 'lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample question-with-value="&' 'quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample ' 'answer-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>' ), ] actual_output_for_ckeditor = ( html_validation_service.validate_rte_format( test_cases_for_ckeditor, feconf.RTE_FORMAT_CKEDITOR ) ) expected_output_for_ckeditor = { 'invalidTags': ['i', 'b'], 'oppia-noninteractive-image': ['b'], 'p': ['pre'], 'strings': [ ( '<oppia-noninteractive-collapsible ' 'heading-with-value="&quot;' 'lorem ipsum&quot;lorem ipsum&quot;?&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-workedexample ' 'question-with-value="&' 'quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample ' 'answer-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<pre>Hello this is <b> testing ' '<oppia-noninteractive-image filepath-with-value="amp;quot;' 'random.png&quot;"></oppia-noninteractive-image> in ' '</b>progress</pre>' ), ( '<oppia-noninteractive-collapsible content-with-value="&' ';quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" heading-with-value="&quot;' 'lorem ipsum&quot;lorem ipsum&quot;?&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value' '=\"[{&quot;content&quot;:&quot;&lt;p&' 'gt;&lt;i&gt;lorem ipsum&lt;/i&gt;&lt;' '/p&gt;&quot;,&quot;title&quot;:&' 'quot;hello&quot;}]\"></oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-workedexample ' 'question-with-value="&' 'quot;&lt;pre&gt;&lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" answer-with-value="&quot;' 'lorem ipsum&quot;">' '</oppia-noninteractive-collapsible>' ), ], } self.assertItemsEqual( actual_output_for_ckeditor, expected_output_for_ckeditor ) def test_validate_soup_for_rte(self) -> None: test_cases_for_textangular = [ ( '<p>Hello <b>this </b>is </p><p><br></p><p>test <b>case ' '</b>for </p><p><oppia-noninteractive-collapsible ' 'content-with-value=\"&quot;Hello oppia&quot;\" ' 'heading-with-value=\"&quot;Learn more about APIs&' 'amp;quot;\"></oppia-noninteractive-collapsible><br></p><p>' 'for migration testing</p>' ), 'Hello<div>oppia</div>testing <i>in progess</i>!', '<p>Hello</p><p>oppia</p><p>testing <i>in progress</i>!</p>', 'Text with no parent tag', '<h1>This is not a allowed tag</h1>', ( '<p><blockquote>Parent child relation not valid</blockquote>' '</p><b><blockquote>Parent child relation not valid' '</blockquote></b>' ), ] expected_output_for_textangular = [False, True, False, True, True, True] err_dict: Dict[str, List[str]] = {} for index, test_case in enumerate(test_cases_for_textangular): actual_output_for_textangular = ( html_validation_service.validate_soup_for_rte( bs4.BeautifulSoup(test_case, 'html.parser'), feconf.RTE_FORMAT_TEXTANGULAR, err_dict, ) ) self.assertEqual( actual_output_for_textangular, expected_output_for_textangular[index], ) test_cases_for_ckeditor = [ ('<p>Lorem ipsum </p><p> Hello this is oppia </p>'), ( '<p>Lorem <span>ipsum </span></p> Hello this is ' '<code>oppia </code>' ), ( '<p><oppia-noninteractive-image filepath-with-value="amp;quot;' 'random.png&quot;"></oppia-noninteractive-image>Hello this ' 'is test case to check image tag inside p tag</p>' ), ] expected_output_for_ckeditor = [False, True, True] err_dict = {} for index, test_case in enumerate(test_cases_for_ckeditor): actual_output_for_ckeditor = ( html_validation_service.validate_soup_for_rte( bs4.BeautifulSoup(test_case, 'html.parser'), feconf.RTE_FORMAT_CKEDITOR, err_dict, ) ) self.assertEqual( actual_output_for_ckeditor, expected_output_for_ckeditor[index] ) def test_validate_customization_args(self) -> None: test_cases = [ ( '<p><oppia-noninteractive-link text-with-value="&quot;What is ' 'a link?&quot;" url-with-value="&quot;htt://link.com&' ';quot;"></oppia-noninteractive-link></p>' ), ( '<p><oppia-noninteractive-link text-with-value="3456" ' 'url-with-value="&quot;http://google.com&' ';quot;"></oppia-noninteractive-link></p>' ), ( '<p><oppia-noninteractive-link text-with-value="&quot;What is ' 'a link?&quot;" url-with-value="&quot;https://link.com&' ';quot;"></oppia-noninteractive-link></p>' ), ( '<oppia-noninteractive-collapsible content-with-value="' '&quot;&lt;p&gt;&lt;oppia-noninteractive-link ' 'url-with-value=\\&quot;&amp;amp;quot;' 'https://www.example.com&amp;amp;quot;\\&quot;&gt;' '&lt;/oppia-noninteractive-link&gt;&lt;/p&gt;' '&quot;" heading-with-value="&quot;Hello&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-image caption-with-value="&quot;' 'abc&quot;" filepath-with-value="&quot;' 'random.png&quot;"></oppia-noninteractive-image>' ), ( '<p><oppia-noninteractive-math math_content-with-value="' '{&quot;raw_latex&quot;:&quot;abc&quot;' ',&quot;svg_filename&quot;:&quot;mathImg_202' '07261338_r3ir43lmfd_height_2d456_width_6d124_vertical_0' 'd231.svg&quot;}"></oppia-noninteractive-math></p>' ), ( '<p><oppia-noninteractive-math url-with-value="&quot;' 'http://link.com&quot;></oppia-noninteractive-math></p>' ), ( '<oppia-noninteractive-collapsible content-with-value=' '"&quot;&lt;p&gt;lorem ipsum&lt;/p&gt;&' 'quot;" heading-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-collapsible content-with-value=' '"34454" heading-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-collapsible content-with-value="' '&quot;&lt;oppia-noninteractive-tabs tab_contents' '-with-value=\\&quot;[{&amp;amp;quot;title&amp;amp;' 'quot;:&amp;amp;quot;Tab&amp;amp;quot;,&amp;amp;quot;' 'content&amp;amp;quot;:&amp;amp;quot;&amp;amp;lt;p&' ';amp;amp;gt;Hello&amp;amp;lt;/p&amp;amp;gt;&amp;' 'amp;quot;}]\\&quot;&gt;&lt;/oppia-noninteractive-tabs' '&gt;&lt;p&gt;You have opened the collapsible block.' '&lt;/p&gt;&quot;" heading-with-value="&quot;' 'Hello&quot;"></oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-collapsible content-with-value=' '"&quot;&lt;oppia-noninteractive-collapsible ' 'content-with-value=\\&quot;&amp;amp;quot;&' 'amp;amp;lt;p&amp;amp;gt;Hello&amp;amp;lt;/p' '&amp;amp;gt;&amp;amp;quot;\\&quot; ' 'heading-with-value=\\&quot;&amp;amp;quot;' 'SubCollapsible&amp;amp;quot;\\&quot;&' 'gt;&lt;/oppia-noninteractive-collapsible&' 'gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;' '&quot;" heading-with-value="&quot;Collapsible' '&quot;"></oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="' '[{&quot;content&quot;: &quot;&lt;p&gt;lorem ' 'ipsum&lt;/p&gt;&quot;, &quot;title&quot;: ' '&quot;hello&quot;}, {&quot;content&quot;: &' 'quot;&lt;p&gt;oppia&lt;/p&gt;&quot;, &' 'quot;title&quot;: &quot;Savjet 1&quot;}]">' '</oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="' '[{&quot;content&quot;: 1234, ' '&quot;title&quot;: &quot;hello&quot;}, ' '{&quot;content&quot;: &quot;&lt;p&gt;' 'oppia&lt;/p&gt;&quot;, &' 'quot;title&quot;: &quot;Savjet 1&quot;}]">' '</oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="' '[{&quot;content&quot;: &quot;&lt;p&gt;lorem ' 'ipsum&lt;/p&gt;&quot;, &quot;url&quot;: ' '&quot;hello&quot;}, {&quot;content&quot;: &' 'quot;&lt;p&gt;oppia&lt;/p&gt;&quot;, &' 'quot;title&quot;: &quot;Savjet 1&quot;}]">' '</oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="' '[{&quot;content&quot;: &quot;&lt;p&gt;lorem ' 'ipsum&lt;/p&gt;&quot;, &quot;title&quot;: ' '&quot;hello&quot;}, [1,2,3]]">' '</oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;' 'title&quot;:&quot;Tab&quot;,&quot;content&' 'quot;:&quot;&lt;oppia-noninteractive-tabs ' 'tab_contents-with-value=\\&quot;[{&amp;amp;quot;title' '&amp;amp;quot;:&amp;amp;quot;Subtab&amp;amp;quot;' ',&amp;amp;quot;content&amp;amp;quot;:&amp;amp;quot;' '&amp;amp;lt;p&amp;amp;gt;Hello&amp;amp;lt;/p&' 'amp;amp;gt;&amp;amp;quot;}]\\&quot;&gt;&lt;' '/oppia-noninteractive-tabs&gt;&lt;p&gt;&amp;' 'nbsp;&lt;/p&gt;&quot;}]">' '</oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="0">' '</oppia-noninteractive-video>' ), ( '<oppia-noninteractive-video autoplay-with-value="&quot;hello' '&quot;" end-with-value="0" start-with-value="0" ' 'video_id-with-value="&quot;loremipsum&quot;">' '</oppia-noninteractive-video>' ), ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="&quot;Hello&quot;" ' 'video_id-with-value="&quot;loremipsum&quot;">' '</oppia-noninteractive-video>' ), ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="0" ' 'video_id-with-value="&quot;lorem&quot;">' '</oppia-noninteractive-video>' ), ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="0" ' 'video_id-with-value="&quot;12345678901&quot;">' '</oppia-noninteractive-video>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;xyz.png&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" ' 'caption-with-value="&quot;Hello&quot;" ' 'filepath-with-value="&quot;xy.z.png&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" ' 'caption-with-value="34454" ' 'filepath-with-value="&quot;xyz.png&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" ' 'caption-with-value="&quot;5454&quot;" ' 'filepath-with-value="&quot;xyz.jpg&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" ' 'caption-with-value="&quot;Hello&quot;" ' 'filepath-with-value="&quot;46503*.jpg&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;' 'title&quot;:&quot;Hint introduction&quot;,&quot;' 'content&quot;:&quot;&lt;p&gt;&lt;' 'oppia-noninteractive-link url-with-value=\\&quot;&amp;amp;' 'quot;https://www.oppia.org&amp;amp;quot;\\&quot;&gt;' '&lt;/oppia-noninteractive-link&gt;&lt;/p&gt;&' 'quot;}]"></oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-workedexample question-with-value=' '"34455" answer-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample question-with-value=' '"&quot;lorem ipsum&quot;" answer-with-value="34456">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample question-with-value="' '&quot;&lt;p&gt;&lt;oppia-noninteractive-link ' 'url-with-value=\\&quot;&amp;amp;quot;' 'https://www.example.com&amp;amp;quot;\\&quot;&gt;' '&lt;/oppia-noninteractive-link&gt;&lt;/p&gt;' '&quot;" answer-with-value="&quot;Hello&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample question-with-value="' '&quot;Hello&quot;" answer-with-value="' '&quot;&lt;p&gt;&lt;oppia-noninteractive-link ' 'url-with-value=\\&quot;&amp;amp;quot;' 'https://www.example.com&amp;amp;quot;\\&quot;&gt;' '&lt;/oppia-noninteractive-link&gt;&lt;/p&gt;' '&quot;">' '</oppia-noninteractive-workedexample>' ), ] actual_output = html_validation_service.validate_customization_args( test_cases ) expected_output = { 'Invalid filepath': [ ( '<oppia-noninteractive-image alt-with-value="&quot;' 'A circle divided into equal fifths.&quot;" caption-' 'with-value="&quot;Hello&quot;" filepath-with-value' '="&quot;46503*.jpg&quot;">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image alt-with-value="&quot;A ' 'circle divided into equal fifths.&quot;" caption-' 'with-value="&quot;Hello&quot;" filepath-with-value' '="&quot;xy.z.png&quot;"></oppia-noninteractive-image>' ), ], 'Expected dict, received [1, 2, 3]': [ ( '<oppia-noninteractive-tabs tab_contents-with-value=' '"[{&quot;content&quot;: &quot;&lt;p&' 'gt;lorem ipsum&lt;/p&gt;&quot;, &quot;' 'title&quot;: &quot;hello&quot;}, [1,2,3]]">' '</oppia-noninteractive-tabs>' ) ], 'Nested tabs and collapsible': [ ( '<oppia-noninteractive-collapsible content-with-value="&' 'quot;&lt;oppia-noninteractive-collapsible content-with-' 'value=\\&quot;&amp;amp;quot;&amp;amp;lt;p&' 'amp;amp;gt;Hello&amp;amp;lt;/p&amp;amp;gt;&amp;' 'amp;quot;\\&quot; heading-with-value=\\&quot;&' 'amp;amp;quot;SubCollapsible&amp;amp;quot;\\&quot;&' 'gt;&lt;/oppia-noninteractive-collapsible&gt;&lt;p' '&gt;&amp;nbsp;&lt;/p&gt;&quot;" ' 'heading-with-value="&quot;Collapsible&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-collapsible content-with-value=' '"&quot;&lt;oppia-noninteractive-tabs tab_contents-' 'with-value=\\&quot;[{&amp;amp;quot;title&amp;' 'amp;quot;:&amp;amp;quot;Tab&amp;amp;quot;,&' 'amp;amp;quot;content&amp;amp;quot;:&amp;amp;quot;' '&amp;amp;lt;p&amp;amp;gt;Hello&amp;amp;lt;/p' '&amp;amp;gt;&amp;amp;quot;}]\\&quot;&gt;&' 'lt;/oppia-noninteractive-tabs&gt;&lt;p&gt;You ' 'have opened the collapsible block.&lt;/p&gt;' '&quot;" heading-with-value="&quot;Hello&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value' '="[{&quot;title&quot;:&quot;Tab&quot;,' '&quot;content&quot;:&quot;&lt;oppia-' 'noninteractive-tabs tab_contents-with-value=\\&quot;' '[{&amp;amp;quot;title&amp;amp;quot;:&amp;amp;' 'quot;Subtab&amp;amp;quot;,&amp;amp;quot;content&' 'amp;amp;quot;:&amp;amp;quot;&amp;amp;lt;p&amp;' 'amp;gt;Hello&amp;amp;lt;/p&amp;amp;gt;&amp;' 'amp;quot;}]\\&quot;&gt;&lt;/oppia-noninteractive' '-tabs&gt;&lt;p&gt;&amp;nbsp;&lt;/p&' 'gt;&quot;}]"></oppia-noninteractive-tabs>' ), ], 'Expected unicode HTML string, received 34454': [ ( '<oppia-noninteractive-collapsible content-with-value="34454" ' 'heading-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-collapsible>' ) ], 'Missing attributes: text-with-value, Extra attributes: ': [ ( '<oppia-noninteractive-collapsible content-with-value' '="&quot;&lt;p&gt;&lt;oppia-noninteractive-' 'link url-with-value=\\&quot;&amp;amp;quot;https://' 'www.example.com&amp;amp;quot;\\&quot;&gt;&lt;' '/oppia-noninteractive-link&gt;&lt;/p&gt;&' 'quot;" heading-with-value="&quot;Hello&quot;">' '</oppia-noninteractive-collapsible>' ), ( '<oppia-noninteractive-tabs tab_contents-with-value="[{&' 'quot;title&quot;:&quot;Hint introduction&quot;,' '&quot;content&quot;:&quot;&lt;p&gt;&' 'lt;oppia-noninteractive-link url-with-value=\\&quot;&' 'amp;amp;quot;https://www.oppia.org&amp;amp;quot;\\&' 'quot;&gt;&lt;/oppia-noninteractive-link&gt;&' 'lt;/p&gt;&quot;}]"></oppia-noninteractive-tabs>' ), ( '<oppia-noninteractive-workedexample question-with-value' '="&quot;&lt;p&gt;&lt;oppia-noninteractive-' 'link url-with-value=\\&quot;&amp;amp;quot;https://' 'www.example.com&amp;amp;quot;\\&quot;&gt;&lt;' '/oppia-noninteractive-link&gt;&lt;/p&gt;&' 'quot;" answer-with-value="&quot;Hello&quot;">' '</oppia-noninteractive-workedexample>' ), ( '<oppia-noninteractive-workedexample question-with-value' '="&quot;Hello&quot;" answer-with-value="' '&quot;&lt;p&gt;&lt;oppia-noninteractive-' 'link url-with-value=\\&quot;&amp;amp;quot;https://' 'www.example.com&amp;amp;quot;\\&quot;&gt;&lt;' '/oppia-noninteractive-link&gt;&lt;/p&gt;&' 'quot;">' '</oppia-noninteractive-workedexample>' ), ], 'Expected bool, received hello': [ ( '<oppia-noninteractive-video autoplay-with-value="&quot;' 'hello&quot;" end-with-value="0" start-with-value="0" ' 'video_id-with-value="&quot;loremipsum&quot;">' '</oppia-noninteractive-video>' ) ], ( 'Invalid URL: Sanitized URL should start with \'http://\' or ' '\'https://\'; received htt://link.com' ): [ ( '<p><oppia-noninteractive-link text-with-value="&quot;' 'What is a link?&quot;" url-with-value="&quot;htt://' 'link.com&quot;"></oppia-noninteractive-link></p>' ) ], ( 'Missing attributes: video_id-with-value, ' 'Extra attributes: ' ): [ ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="0">' '</oppia-noninteractive-video>' ) ], 'Expected unicode string, received 34454': [ ( '<oppia-noninteractive-image alt-with-value="&quot;' 'A circle divided into equal fifths.&quot;" ' 'caption-with-value="34454" filepath-with-value="&quot;' 'xyz.png&quot;"></oppia-noninteractive-image>' ) ], 'Expected unicode string, received 3456': [ ( '<p><oppia-noninteractive-link text-with-value="3456" ' 'url-with-value="&quot;http://google.com&quot;">' '</oppia-noninteractive-link></p>' ) ], 'Missing keys: [\'title\'], Extra keys: [\'url\']': [ ( '<oppia-noninteractive-tabs tab_contents-with-value="' '[{&quot;content&quot;: &quot;&lt;p&' 'gt;lorem ipsum&lt;/p&gt;&quot;, &quot;url' '&quot;: &quot;hello&quot;}, {&quot;' 'content&quot;: &quot;&lt;p&gt;oppia' '&lt;/p&gt;&quot;, &quot;title&quot;: ' '&quot;Savjet 1&quot;}]"></oppia-noninteractive-tabs>' ) ], 'Could not convert str to int: Hello': [ ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="&quot;Hello&' 'quot;" video_id-with-value="&quot;loremipsum&quot;">' '</oppia-noninteractive-video>' ) ], 'Expected unicode HTML string, received 1234': [ ( '<oppia-noninteractive-tabs tab_contents-with-value=' '"[{&quot;content&quot;: 1234, &quot;title' '&quot;: &quot;hello&quot;}, {&quot;' 'content&quot;: &quot;&lt;p&gt;oppia&' 'amp;lt;/p&gt;&quot;, &quot;title&quot;: ' '&quot;Savjet 1&quot;}]"></oppia-noninteractive-tabs>' ) ], 'Missing attributes: alt-with-value, Extra attributes: ': [ ( '<oppia-noninteractive-image caption-with-value="&quot;abc' '&quot;" filepath-with-value="&quot;random.png&' 'quot;"></oppia-noninteractive-image>' ) ], 'Video id length is not 11': [ ( '<oppia-noninteractive-video autoplay-with-value="false" ' 'end-with-value="0" start-with-value="0" video_id-with-value="' '&quot;lorem&quot;"></oppia-noninteractive-video>' ) ], 'Expected unicode HTML string, received 34455': [ ( '<oppia-noninteractive-workedexample question-with-value=' '"34455" answer-with-value="&quot;lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>' ) ], 'Expected unicode HTML string, received 34456': [ ( '<oppia-noninteractive-workedexample question-with-value=' '"&quot;lorem ipsum&quot;" answer-with-value="34456">' '</oppia-noninteractive-workedexample>' ) ], } self.assertEqual(set(actual_output.keys()), set(expected_output.keys())) for key, expected in expected_output.items(): self.assertEqual(set(actual_output[key]), set(expected)) def test_validate_customization_args_in_tag(self) -> None: test_cases = [ { 'html_string': ( '<p><oppia-noninteractive-link text-with-value="&quot;What ' 'is a link?&quot;" url-with-value="&quot;https://link' '.com&quot;"></oppia-noninteractive-link></p>' ), 'tag_name': 'oppia-noninteractive-link', }, { 'html_string': ( '<p><oppia-noninteractive-link text-with-value="&quot;' 'What is a link?&quot;" url-with-value="&quot;' 'htt://link.com&quot;"></oppia-noninteractive-link></p>' ), 'tag_name': 'oppia-noninteractive-link', }, { 'html_string': ( '<oppia-noninteractive-image caption-with-value="&quot;' 'abc&quot;" filepath-with-value="&quot;' 'random.png&quot;"></oppia-noninteractive-image>' ), 'tag_name': 'oppia-noninteractive-image', }, { 'html_string': ( '<oppia-noninteractive-tabs tab_contents-with-value="[{&' 'quot;content&quot;: &quot;&lt;p&gt;lorem' 'ipsum&lt;/p&gt;&quot;, &quot;title&quot;: ' '&quot;hello&quot;}, [1,2,3]]">' '</oppia-noninteractive-tabs>' ), 'tag_name': 'oppia-noninteractive-tabs', }, ] actual_output = [] expected_output = [ [], [ ( 'Invalid URL: Sanitized URL should start with \'http://\' ' 'or \'https://\'; received htt://link.com' ) ], ['Missing attributes: alt-with-value, Extra attributes: '], ['Expected dict, received [1, 2, 3]'], ] for test_case in test_cases: html_string = test_case['html_string'] tag_name = test_case['tag_name'] soup = bs4.BeautifulSoup( html_string.encode(encoding='utf-8'), 'html.parser' ) actual_output.append( list( html_validation_service.validate_customization_args_in_tag( soup.find(name=tag_name) ) ) ) self.assertEqual(actual_output, expected_output) def test_svg_string_validation(self) -> None: # A Valid SVG string. valid_svg_string = ( '<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="' '100pt" height="100pt" viewBox="0 0 100 100"><g><path d="M5455 ' '2632 9z"/> </g> </svg>' ) self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( valid_svg_string ), ([], []), ) # A Valid SVG string with unicode characters. valid_svg_string_with_unicode = ( '<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="' '100pt" height="100pt" viewBox="0 0 100 100"><g><path d="M5455 ' '2632 9z"/></g><text transform="matrix(1 0 0 -1 0 0)" font-size' '="884px" font-family="serif">Ì</text></svg>' ) self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( valid_svg_string_with_unicode ), ([], []), ) # SVG containing an invalid tag. invalid_svg_string = '<svg><testtag /></svg>' self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( invalid_svg_string ), (['testtag'], []), ) # SVG containing an invalid attribute for a valid tag. invalid_svg_string = '<svg><path d="M5455" danger="h4cK3D!" /></svg>' self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( invalid_svg_string ), ([], ['path:danger']), ) # SVG containing an invalid attribute in an invalid tag. invalid_svg_string = '<svg><hack d="M5 1z" danger="XYZ!"></svg>' self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( invalid_svg_string ), (['hack'], []), ) # SVG containing a valid tag masquerading as an attribute. invalid_svg_string = '<svg><g fill="#FFFFFF" path="YZ!" /></svg>' self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( invalid_svg_string ), ([], ['g:path']), ) # SVG containing a invalid attribute for the parent tag but valid for # a different tag. invalid_svg_string = '<svg><path d="M5455" keytimes="h4cK3D!"></svg>' self.assertEqual( html_validation_service.get_invalid_svg_tags_and_attrs( invalid_svg_string ), ([], ['path:keytimes']), ) def test_svg_tag_without_xmlns_attribute(self) -> None: # A valid SVG string with xmlns_attribute. valid_svg_string = ( '<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="' '100pt" height="100pt" viewBox="0 0 100 100"><g><path d="M5455 ' '2632 9z"/></g><text transform="matrix(1 0 0 -1 0 0)" font-size' '="884px" font-family="serif">Ì</text></svg>' ) self.assertTrue( html_validation_service.does_svg_tag_contains_xmlns_attribute( valid_svg_string ) ) # An invalid SVG string without xmlns attribute. invalid_svg_string = ( '<svg version="1.0" width="100pt" height="100pt" ' 'viewBox="0 0 100 100"><g><path d="M5455 ' '2632 9z"/></g><text transform="matrix(1 0 0 -1 0 0)" font-size' '="884px" font-family="serif">Ì</text></svg>' ) self.assertFalse( html_validation_service.does_svg_tag_contains_xmlns_attribute( invalid_svg_string ) ) def test_add_math_content_to_math_rte_components(self) -> None: test_cases = [ { 'html_content': ( '<p>Feedback</p><oppia-noninteractive-math raw_latex-with-valu' 'e="&quot;+,-,-,+&quot;"></oppia-noninteractive-math>' ), 'expected_output': ( '<p>Feedback</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot;&' ';quot;}"></oppia-noninteractive-math>' ), }, { 'html_content': ( '<oppia-noninteractive-math raw_latex-with-value="&quot;+,' '+,+,+&quot;"></oppia-noninteractive-math>' ), 'expected_output': ( '<oppia-noninteractive-math math_content-with-value="{&' 'quot;raw_latex&quot;: &quot;+,+,+,+&quot;, &' 'quot;svg_filename&quot;: &quot;&quot;}"></oppia' '-noninteractive-math>' ), }, { 'html_content': ( '<oppia-noninteractive-math raw_latex-with-value="&quot;' '(x - a_1)(x - a_2)(x - a_3)...(x - a_n)&quot;"></oppia-' 'noninteractive-math>' ), 'expected_output': ( '<oppia-noninteractive-math math_content-with-value="{&q' 'uot;raw_latex&quot;: &quot;(x - a_1)(x - a_2)(x - a' '_3)...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' ), }, { 'html_content': '<p> This is a normal tag </p>', 'expected_output': '<p> This is a normal tag </p>', }, { 'html_content': ( '<oppia-noninteractive-math math_content-with-value="{&qu' 'ot;raw_latex&quot;: &quot;(x - a_1)(x - a_2)(x - a_3)' '...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' ), 'expected_output': ( '<oppia-noninteractive-math math_content-with-value="{&q' 'uot;raw_latex&quot;: &quot;(x - a_1)(x - a_2)(x - a' '_3)...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' ), }, { # The empty math tag here just gets deleted. 'html_content': ( '<p>Feedback</p><oppia-noninteractive-math></oppia-nonintera' 'ctive-math>' ), 'expected_output': '<p>Feedback</p>', }, { # If the raw_latex field is empty, the entire math tag gets # deleted. 'html_content': ( '<oppia-noninteractive-math raw_latex-with-value="">' '</oppia-noninteractive-math>blahblah' ), 'expected_output': 'blahblah', }, ] for test_case in test_cases: self.assertEqual( html_validation_service.add_math_content_to_math_rte_components( test_case['html_content'] ), test_case['expected_output'], ) invalid_cases = [ { 'html_content': ( '<p>Feedback</p><oppia-noninteractive-math raw_latex-with-valu' 'e="++--"></oppia-noninteractive-math>' ) } ] with self.assertRaisesRegex( Exception, re.escape('Expecting value: line 1 column 1 (char 0)') ): html_validation_service.add_math_content_to_math_rte_components( invalid_cases[0]['html_content'] ) def test_validate_math_tags_in_html(self) -> None: """Test that the validate_math_tags_in_html method validates an HTML string and returns all the invalid tags. """ html_string = ( '<p>Feedback</p><oppia-noninteractive-math raw_latex-with-valu' 'e="+,-,-,+"></oppia-noninteractive-math><p>Feedback</p><oppia-n' 'oninteractive-math></oppia-noninteractive-math><p>Feedback</p><' 'oppia-noninteractive-math invalid_tag-with-value="&quot;+,-' ',-,+&quot;"></oppia-noninteractive-math><p>Feedback</p><opp' 'ia-noninteractive-math raw_latex-with-value="&quot;+,-,-,+&' 'amp;quot;"></oppia-noninteractive-math><p>Feedback</p><oppia-no' 'ninteractive-math raw_latex-with-value="&quot;+,-,-,+&q' 'uot;"></oppia-noninteractive-math>' ) expected_invalid_tags = [ ( '<oppia-noninteractive-math raw_latex-with-value="+,-,-,+"></op' 'pia-noninteractive-math>' ), ('<oppia-noninteractive-math></oppia-noninteractive-math>'), ( '<oppia-noninteractive-math invalid_tag-with-value="&quot;+' ',-,-,+&quot;"></oppia-noninteractive-math>' ), ( '<oppia-noninteractive-math raw_latex-with-value="&quot;+,-' ',-,+&quot;"></oppia-noninteractive-math>' ), ] invalid_tags = html_validation_service.validate_math_tags_in_html( html_string ) for index, invalid_tag in enumerate(invalid_tags): self.assertEqual(str(invalid_tag), expected_invalid_tags[index]) def test_validate_math_tags_in_html_with_attribute_math_content( self, ) -> None: """Test that the validate_math_tags_in_html_with_attribute_math_content method validates an HTML string and returns all the invalid tags. """ html_string = ( '<p>Feedback</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot;&' ';quot;}"></oppia-noninteractive-math>' '<oppia-noninteractive-math raw_latex-with-value="&quot;+,' '+,+,+&quot;"></oppia-noninteractive-math>' '<oppia-noninteractive-math raw_latex-with-value="&quot;' '(x - a_1)(x - a_2)(x - a_3)...(x - a_n)&quot;"></oppia-' 'noninteractive-math>' '<oppia-noninteractive-math math_content-with-value="{&q' 'uot;raw_latex&quot;: &quot;(x - a_1)(x - a_2)(x - a' '_3)...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' '<oppia-noninteractive-math></oppia-noninteractive-math>' '<p>this is a normal tag</p>' '<oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;}"></oppia-noninteractive-math>' '<oppia-noninteractive-math math_content-with-value="{' 'raw_latex: &quot;(x - a_1)(x - a_2)(x - a' '_3)...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' ) expected_invalid_tags = [ ( '<oppia-noninteractive-math raw_latex-with-value="&quot;' '(x - a_1)(x - a_2)(x - a_3)...(x - a_n)&quot;"></oppia-' 'noninteractive-math>' ), ('<oppia-noninteractive-math></oppia-noninteractive-math>'), ( '<oppia-noninteractive-math raw_latex-with-value="&quot;+,' '+,+,+&quot;"></oppia-noninteractive-math>' ), ( '<oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;}"></oppia-noninteractive-math>' ), ( '<oppia-noninteractive-math math_content-with-value="{' 'raw_latex: &quot;(x - a_1)(x - a_2)(x - a' '_3)...(x - a_n)&quot;, &quot;svg_filename&quot;' ': &quot;&quot;}"></oppia-noninteractive-math>' ), ] invalid_tags = html_validation_service.validate_math_tags_in_html_with_attribute_math_content( html_string ) self.assertEqual(len(invalid_tags), 5) for invalid_tag in invalid_tags: self.assertIn(str(invalid_tag), expected_invalid_tags) def test_extract_svg_filenames_in_math_rte_components(self) -> None: """Test that the extract_svg_filenames_in_math_rte_components method extracts all the filenames from math rich-text components in html. """ html_string_with_filename_having_filename = ( '<p>Feedback</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img.svg&quot;}"></oppia-noninteractive-math>' ) self.assertEqual( html_validation_service.extract_svg_filenames_in_math_rte_components( html_string_with_filename_having_filename ), ['img.svg'], ) html_string_with_no_filename = ( '<p>Feedback</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';&quot;}"></oppia-noninteractive-math>' ) self.assertEqual( html_validation_service.extract_svg_filenames_in_math_rte_components( html_string_with_no_filename ), [], ) def test_validate_svg_filenames_when_all_filenames_are_valid(self) -> None: """Test the validate_svg_filenames_in_math_rich_text when valid filenames are present for each math rich-text components in html. """ html_string_with_filename_having_filename = ( '<p>Feedback1</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img1.svg&quot;}"></oppia-noninteractive-math>' '<p>Feedback2</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img2.svg&quot;}"></oppia-noninteractive-math>' ) with open( os.path.join(feconf.TESTS_DATA_DIR, 'test_svg.svg'), 'rb', encoding=None, ) as f: raw_image = f.read() fs = fs_services.GcsFileSystem( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1' ) fs.commit('image/img1.svg', raw_image, mimetype='image/svg+xml') fs.commit('image/img2.svg', raw_image, mimetype='image/svg+xml') self.assertEqual( html_validation_service.validate_svg_filenames_in_math_rich_text( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1', html_string_with_filename_having_filename, ), [], ) def test_validate_svg_filenames_when_filenames_are_invalid(self) -> None: """Test the validate_svg_filenames_in_math_rich_text when filenames are present but invalid. """ html_string_with_filename_having_filename = ( '<p>Feedback1</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img1.svg&quot;}"></oppia-noninteractive-math>' '<p>Feedback2</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img2.svg&quot;}"></oppia-noninteractive-math>' ) with open( os.path.join(feconf.TESTS_DATA_DIR, 'test_svg.svg'), 'rb', encoding=None, ) as f: raw_image = f.read() fs = fs_services.GcsFileSystem( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1' ) fs.commit('image/img1.svg', raw_image, mimetype='image/svg+xml') self.assertEqual( html_validation_service.validate_svg_filenames_in_math_rich_text( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1', html_string_with_filename_having_filename, ), [ ( '<oppia-noninteractive-math math_content-with-value="{&' 'amp;quot;raw_latex&quot;: &quot;+,-,-,+&qu' 'ot;, &quot;svg_filename&quot;: &quot;img2.' 'svg&quot;}"></oppia-noninteractive-math>' ) ], ) def test_validate_svg_filenames_when_filenames_are_not_present( self, ) -> None: """Test the validate_svg_filenames_in_math_rich_text when filenames are not present. """ html_string_with_filename_having_filename = ( '<p>Feedback1</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';img1.svg&quot;}"></oppia-noninteractive-math>' '<p>Feedback2</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';&quot;}"></oppia-noninteractive-math>' ) with open( os.path.join(feconf.TESTS_DATA_DIR, 'test_svg.svg'), 'rb', encoding=None, ) as f: raw_image = f.read() fs = fs_services.GcsFileSystem( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1' ) fs.commit('image/img1.svg', raw_image, mimetype='image/svg+xml') self.assertEqual( html_validation_service.validate_svg_filenames_in_math_rich_text( feconf.ENTITY_TYPE_EXPLORATION, 'exp_id1', html_string_with_filename_having_filename, ), [ ( '<oppia-noninteractive-math math_content-with-value="{&' 'amp;quot;raw_latex&quot;: &quot;+,-,-,+&qu' 'ot;, &quot;svg_filename&quot;: &quot;' '&quot;}"></oppia-noninteractive-math>' ) ], ) def test_validate_svg_filenames_format_when_all_filenames_are_valid( self, ) -> None: """Test the validate_svg_filenames_in_math_rich_text when valid filenames are present for each math rich-text components in html. """ html_string_with_filename_having_valid_format = ( '<p>Feedback1</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';mathImg_20201216_331234_r3ir43lmfd_height_2d456_width_6d1' '24_vertical_0d231.svg&quot;}"></oppia-noninteractive-math>' '<p>Feedback2</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';mathImg_20200216_133832_imzlvnf23a_height_4d123_width_23d' '122_vertical_2d123.svg&quot;}"></oppia-noninteractive-math>' ) self.assertEqual( html_validation_service.validate_math_content_attribute_in_html( html_string_with_filename_having_valid_format ), [], ) def test_validate_svg_filenames_format_when_all_filenames_are_invalid( self, ) -> None: """Test the validate_svg_filenames_in_math_rich_text when valid filenames are present for each math rich-text components in html. """ html_string_with_filename_having_invalid_format = ( '<p>Feedback1</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';mathImg_20201216*331234_r3ir43lmfd_height_2d456_width_6d1' '24_vertical_0d231.svg&quot;}"></oppia-noninteractive-math>' '<p>Feedback2</p><oppia-noninteractive-math math_content-with-v' 'alue="{&quot;raw_latex&quot;: &quot;+,-,-,+' '&quot;, &quot;svg_filename&quot;: &quot' ';mathImg_20200216_133832_imzlvnf23a_invalid_4d123_width_23d' '122_vertical_2d123.svg&quot;}"></oppia-noninteractive-math>' ) expected_output = [ { 'invalid_tag': ( '<oppia-noninteractive-math math_content-with-value="{&am' 'p;quot;raw_latex&quot;: &quot;+,-,-,+&quot;, ' '&quot;svg_filename&quot;: &quot;mathImg_20201' '216*331234_r3ir43lmfd_height_2d456_width_6d124_vertical_0' 'd231.svg&quot;}"></oppia-noninteractive-math>' ), 'error': ( 'Invalid svg_filename attribute in math component: ' 'mathImg_20201216*331234_r3ir43lmfd_height_2d456_width_6d1' '24_vertical_0d231.svg' ), }, { 'invalid_tag': ( '<oppia-noninteractive-math math_content-with-value="{&' 'quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &a' 'mp;quot;svg_filename&quot;: &quot;mathImg_2020021' '6_133832_imzlvnf23a_invalid_4d123_width_23d122_vertical_2' 'd123.svg&quot;}"></oppia-noninteractive-math>' ), 'error': ( 'Invalid svg_filename attribute in math component: ' 'mathImg_20200216_133832_imzlvnf23a_inv' 'alid_4d123_width_23d122_vertical_2d123.svg' ), }, ] self.assertItemsEqual( html_validation_service.validate_math_content_attribute_in_html( html_string_with_filename_having_invalid_format ), expected_output, ) def test_check_for_svgdiagram_component_in_html(self) -> None: """Test that the check_for_svgdiagram_component_in_html method checks for math-tags in an HTML string and returns a boolean. """ test_cases: List[SvgDiagramTestCaseDict] = [ { 'html_content': ( '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value="&quot;img1.svg&quot;"' ' alt-with-value="&quot;Image&quot;">' '</oppia-noninteractive-svgdiagram>' ), 'expected_output': True, }, { 'html_content': ( '<p><oppia-noninteractive-image filepath-with-value=' '"abc1.png"></oppia-noninteractive-image>Hello this is test ' 'case to check that dimensions are added to the oppia ' 'noninteractive image tags.</p>' ), 'expected_output': False, }, ] for test_case in test_cases: self.assertEqual( html_validation_service.check_for_svgdiagram_component_in_html( test_case['html_content'] ), test_case['expected_output'], ) def test_parsable_as_xml(self) -> None: invalid_xml = b'aDRjSzNS' self.assertEqual( html_validation_service.is_parsable_as_xml(invalid_xml), False ) invalid_xml = b'123' self.assertEqual( html_validation_service.is_parsable_as_xml(invalid_xml), False ) # TODO(#13059): Here we use MyPy ignore because after we fully type the # codebase we plan to get rid of the tests that intentionally test wrong # inputs that we can normally catch by typing. invalid_xml = False # type: ignore[assignment] self.assertEqual( html_validation_service.is_parsable_as_xml(invalid_xml), False ) valid_xml = b'<svg><path d="0" /></svg>' self.assertEqual( html_validation_service.is_parsable_as_xml(valid_xml), True ) def test_convert_svg_diagram_tags_to_image_tags(self) -> None: test_cases = [ { 'html_content': ( '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value="&quot;img1.svg&quot;"' ' alt-with-value="&quot;Image&quot;">' '</oppia-noninteractive-svgdiagram>' ), 'expected_output': ( '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;img1.svg&quot;">' '</oppia-noninteractive-image>' ), }, { 'html_content': ( '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value="&quot;img12.svg&quot;"' ' alt-with-value="&quot;Image&quot;">' '</oppia-noninteractive-svgdiagram>' '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value="&quot;img2.svg&quot;"' ' alt-with-value="&quot;Image123&quot;">' '</oppia-noninteractive-svgdiagram>' '<oppia-noninteractive-svgdiagram ' 'alt-with-value="&quot;Image12345&quot;"' ' svg_filename-with-value="&quot;igage.svg&quot;">' '</oppia-noninteractive-svgdiagram>' ), 'expected_output': ( '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;img12.svg&quot;">' '</oppia-noninteractive-image>' '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image123&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;img2.svg&quot;">' '</oppia-noninteractive-image>' '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image12345&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;igage.svg&quot;">' '</oppia-noninteractive-image>' ), }, { 'html_content': ( r'<oppia-noninteractive-tabs tab_contents-with-value="[{&' r'quot;title&quot;:&quot;Hint introduction&quot;,' r'&quot;content&quot;:&quot;&lt;oppia-' r'noninteractive-svgdiagram alt-with-value=\&quot;' r'&amp;amp;quot;desc&amp;amp;quot;\&quot; ' r'svg_filename-with-value=\&quot;&amp;amp;quot;' r'img_20210727_054514_9l3scri3mg_height_350_width_450.svg&' r'amp;amp;quot;\&quot;&gt;&lt;' r'/oppia-noninteractive-svgdiagram&gt;&quot;},' r'{&quot;title&quot;:' r'&quot;Hint 1&quot;,&quot;content&quot;:&' r'quot;&lt;oppia-noninteractive-svgdiagram alt-with-value=' r'\&quot; \&quot;&amp;amp;quot;abc&amp;' r'amp;quot;ng-version=\&quot;11.2.14\&quot; svg_' r'filename-with-value=\&quot;&amp;amp;quot;' r'img_20210727_054530_g653s2p0af_height_350_width_450.svg' r'&amp;amp;quot;\&quot;&gt;&lt;' r'/oppia-noninteractive-svgdiagram&gt;&quot;}]">' r'</oppia-noninteractive-tabs>' ), 'expected_output': ( r'<oppia-noninteractive-tabs tab_contents-with-value="[{&' r'quot;title&quot;: &quot;Hint introduction&quot;, ' r'&quot;content&quot;: &quot;&lt;' r'oppia-noninteractive-image alt-with-value=\&quot;&' r'amp;amp;quot;desc&amp;amp;quot;\&quot; ' r'caption-with-value=\&quot;&amp;amp;quot;&amp;amp;' r'quot;\&quot; filepath-with-value=\&quot;&amp;amp;' r'quot;img_20210727_054514_9l3scri3mg_height_350_width_450.svg' r'&amp;amp;quot;\&quot;&gt;&lt;' r'/oppia-noninteractive-image&gt;&quot;}, {&quot;' r'title&quot;: &quot;Hint 1&quot;, &quot;' r'content&quot;: &quot;&lt;' r'oppia-noninteractive-image &amp;amp;quot;abc&amp;amp;' r'quot;ng-version=\&quot;11.2.14\&quot; ' r'alt-with-value=\&quot; \&quot; ' r'caption-with-value=\&quot;&amp;amp;quot;&amp;amp;' r'quot;\&quot; filepath-with-value=\&quot;&amp;amp;' r'quot;img_20210727_054530_g653s2p0af_height_350_width_450.svg' r'&amp;amp;quot;\&quot;&gt;&lt;' r'/oppia-noninteractive-image&gt;&quot;}]">' r'</oppia-noninteractive-tabs>' ), }, { 'html_content': ( r'<oppia-noninteractive-collapsible content-with-value="&' r'quot;&lt;oppia-noninteractive-svgdiagram alt-with-value=' r'\&quot;&amp;amp;quot;abc&amp;amp;quot;\&' r'quot; ng-version=\&quot;11.2.14\&quot; ' r'svg_filename-with-value=\&quot;&amp;amp;quot;' r'img_20210727_054955_a9it96co1j_height_350_width_450.svg' r'&amp;amp;quot;\&quot;&gt;&lt;' r'/oppia-noninteractive-svgdiagram&gt;&quot;" ' r'heading-with-value="&quot;Sample Header&quot;">' r'</oppia-noninteractive-collapsible>' ), 'expected_output': ( '<oppia-noninteractive-collapsible content-with-value="&' 'quot;&lt;oppia-noninteractive-image alt-with-value=\\' '&quot;&amp;amp;quot;abc&amp;amp;quot;\\&' 'quot; caption-with-value=\\&quot;&amp;amp;quot;&' 'amp;amp;quot;\\&quot; filepath-with-value=\\&quot;' '&amp;amp;quot;img_20210727_054955_a9it96co1j_height_' '350_width_450.svg&amp;amp;quot;\\&quot; ' 'ng-version=\\&quot;11.2.14\\&quot;&gt;&' 'lt;/oppia-noninteractive-image&gt;&quot;" ' 'heading-with-value="&quot;Sample Header&quot;">' '</oppia-noninteractive-collapsible>' ), }, ] for test_case in test_cases: self.assertEqual( html_validation_service.convert_svg_diagram_tags_to_image_tags( test_case['html_content'] ), test_case['expected_output'], ) def test_no_convertion_of_non_interactive_image_tags(self) -> None: """Test that the convert_svg_diagram_tags_to_image_tags does not make any changes in already existing oppia-noninteractive image tags. """ test_cases = [ { 'html_content': ( '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image1&quot;" ' 'caption-with-value="&quot;xyz&quot;" ' 'filepath-with-value="&quot;img123.svg&quot;">' '</oppia-noninteractive-image>' ), 'expected_output': ( '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image1&quot;" ' 'caption-with-value="&quot;xyz&quot;" ' 'filepath-with-value="&quot;img123.svg&quot;">' '</oppia-noninteractive-image>' ), }, { 'html_content': ( '<oppia-noninteractive-svgdiagram ' 'svg_filename-with-value="&quot;img11.svg&quot;"' ' alt-with-value="&quot;Image&quot;">' '</oppia-noninteractive-svgdiagram>' '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image1&quot;" ' 'caption-with-value="&quot;abcxyz&quot;" ' 'filepath-with-value="&quot;img123.svg&quot;">' '</oppia-noninteractive-image>' ), 'expected_output': ( '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image&quot;" ' 'caption-with-value="&quot;&quot;" ' 'filepath-with-value="&quot;img11.svg&quot;">' '</oppia-noninteractive-image>' '<oppia-noninteractive-image ' 'alt-with-value="&quot;Image1&quot;" ' 'caption-with-value="&quot;abcxyz&quot;" ' 'filepath-with-value="&quot;img123.svg&quot;">' '</oppia-noninteractive-image>' ), }, ] for test_case in test_cases: self.assertEqual( html_validation_service.convert_svg_diagram_tags_to_image_tags( test_case['html_content'] ), test_case['expected_output'], ) def test_fix_incorrectly_encoded_chars_replaces_incorrect_encodings( self, ) -> None: test_cases = [ { 'html_string': '<p>This is <span>testing </span></p>', 'expected_output': '<p>This is <span>testing </span></p>', }, { 'html_string': '<p>This is <span>\t testing \n</span></p>', 'expected_output': '<p>This is <span> testing </span></p>', }, { 'html_string': '<p>Hello this is <span>testing \xa0</span></p>', 'expected_output': '<p>Hello this is <span>testing </span></p>', }, { 'html_string': '<p>Hello this is <span>testing \xc2</span></p>', 'expected_output': '<p>Hello this is <span>testing </span></p>', }, { 'html_string': '<p>Hello this is <span>testing \xe2\u2020\u2019' ' \xe2\u20ac\u0153 \xe2\u02c6\u2030 \xe2\u2026\u02dc ' '\xe2\u20ac\u2122 \xe2\u02c6\u0161 \xe2\u02c6\u02c6 ' '\xe2\u2026\u2022 \xe2\u2026\u2122 \xe2\u20ac\u02dc ' '\xe2\u20ac\u201d \xe2\u20ac\u2039 \xe2\xcb\u2020\xe2\u20ac\xb0' '</span></p>', 'expected_output': '<p>Hello this is <span>testing \u2192 ' '\u201c \u2209 \u2158 \u2019 \u221a \u2208 \u2155 \u2159 ' '\u2018 \u2014 \u200b \u2209</span></p>', }, { 'html_string': '<oppia-noninteractive-workedexample ' 'question-with-value="&' 'quot;&lt;pre&gt;&' 'lt;p&gt;lorem \xc2ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" answer-with-value="&quot;' 'lorem ipsum&quot;">' '</oppia-noninteractive-workedexample>', 'expected_output': '<oppia-noninteractive-workedexample ' 'answer-with-value="&quot;' 'lorem ipsum&quot;" ' 'question-with-value="&' 'quot;&lt;pre&gt;&' ';lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;">' '</oppia-noninteractive-workedexample>', }, { 'html_string': '<oppia-noninteractive-workedexample ' 'question-with-value="&' 'quot;&lt;pre&gt;&' 'lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;" answer-with-value="&quot;' 'lorem \xc2ipsum&quot;">' '</oppia-noninteractive-workedexample>', 'expected_output': '<oppia-noninteractive-workedexample ' 'answer-with-value="&quot;' 'lorem ipsum&quot;" ' 'question-with-value="&' 'quot;&lt;pre&gt;&' 'lt;p&gt;lorem ipsum&' 'lt;/p&gt;&lt;/pre&gt;' '&quot;">' '</oppia-noninteractive-workedexample>', }, ] for test_case in test_cases: self.assertEqual( html_validation_service.fix_incorrectly_encoded_chars( test_case['html_string'] ), test_case['expected_output'], )