Skip to content

Commit

Permalink
test: [FC-0063] Content processors are tested
Browse files Browse the repository at this point in the history
  • Loading branch information
myhailo-chernyshov-rg committed Jan 23, 2025
1 parent b55f918 commit 253b223
Show file tree
Hide file tree
Showing 18 changed files with 488 additions and 323 deletions.
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[pytest]
usefixtures = chdir_to_workspace
DJANGO_SETTINGS_MODULE = cc2olx.django_settings
DJANGO_SETTINGS_MODULE = cc2olx.settings
46 changes: 18 additions & 28 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import shutil
import zipfile

import xml.dom.minidom
from pathlib import Path
from tempfile import NamedTemporaryFile
from xml.dom.minidom import parse
Expand All @@ -13,8 +12,7 @@

from cc2olx.cli import parse_args
from cc2olx.models import Cartridge
from cc2olx.olx import OlxExport
from cc2olx.settings import collect_settings
from cc2olx.parser import parse_options


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -78,30 +76,38 @@ def studio_course_xml(fixtures_data_dir):
return parse(course_xml_filename).toprettyxml()


@pytest.fixture(scope="session")
def relative_links_source() -> str:
"""
Provide a relative links source.
"""
return "https://relative.source.domain"


@pytest.fixture
def settings(imscc_file, link_map_csv):
def options(imscc_file, link_map_csv, relative_links_source):
"""
Basic settings fixture.
Basic options fixture.
"""

parsed_args = parse_args(["-i", str(imscc_file), "-f", str(link_map_csv)])
args = parse_args(["-i", str(imscc_file), "-f", str(link_map_csv), "-s", relative_links_source])

_settings = collect_settings(parsed_args)
options = parse_options(args)

yield _settings
yield options

shutil.rmtree(_settings["workspace"], ignore_errors=True)
shutil.rmtree(options["workspace"], ignore_errors=True)


@pytest.fixture
def cartridge(imscc_file, settings):
cartridge = Cartridge(imscc_file, settings["workspace"])
def cartridge(imscc_file, options):
cartridge = Cartridge(imscc_file, options["workspace"])
cartridge.load_manifest_extracted()
cartridge.normalize()

yield cartridge

shutil.rmtree(str(settings["workspace"] / imscc_file.stem))
shutil.rmtree(str(options["workspace"] / imscc_file.stem))


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -289,19 +295,3 @@ def expected_cleaned_cdata_containing_html(fixtures_data_dir: Path) -> str:
"""
html_without_cdata_path = fixtures_data_dir / "html_files/cleaned-cdata-containing-html.html"
return html_without_cdata_path.read_text()


@pytest.fixture
def bare_olx_exporter(cartridge: Cartridge) -> OlxExport:
"""
Provides bare OLX exporter.
Args:
cartridge (Cartridge): Cartridge class instance.
Returns:
OlxExport: OlxExport instance.
"""
olx_exporter = OlxExport(cartridge)
olx_exporter.doc = xml.dom.minidom.Document()
return olx_exporter
2 changes: 1 addition & 1 deletion tests/fixtures_data/imscc_file/web_link_content.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<webLink xmlns="http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imsccv1p3/imswl_v1p3 http://www.imsglobal.org/profile/cc/ccv1p3/ccv1p3_imswl_v1p3.xsd">
<title>Web Link Content</title>
<url href="http://web-link"/>
<url href="/web-link"/>
</webLink>
26 changes: 23 additions & 3 deletions tests/fixtures_data/studio_course_xml/course.xml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,17 @@
<discussion display_name="Discussion" discussion_category="Discussion Topic" discussion_target="Discussion Topic" url_name="discussion_topic"/>
</vertical>
<vertical display_name="Image File Webcontent" url_name="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
<html display_name="Image File Webcontent" url_name="resource_5_image_file"><![CDATA[<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body><p><img src="/static/elearning.png" alt="elearning.png"></p></body></html>]]></html>
<html display_name="Image File Webcontent" url_name="resource_5_image_file"><![CDATA[<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<p>
<img src="/static/elearning.png" alt="elearning.png">
</p>
</body>
</html>
]]></html>
</vertical>
<vertical display_name="Wiki Content" url_name="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
<html display_name="Wiki Content" url_name="resource_6_wiki_content"><![CDATA[<html>
Expand Down Expand Up @@ -227,10 +237,20 @@
<video edx_video_id="42d2a5e2-bced-45d6-b8dc-2f5901c9fdd0" display_name="Video With Other Content" url_name="resource_9_video"/>
</vertical>
<vertical display_name="PDF Outside of Web Resources" url_name="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
<html display_name="PDF Outside of Web Resources" url_name="pdf_dependency"><![CDATA[<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body><p><a href="/static/extra_files/example.pdf" alt="extra_files/example.pdf">extra_files/example.pdf<a></p></body></html>]]></html>
<html display_name="PDF Outside of Web Resources" url_name="pdf_dependency"><![CDATA[<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<p>
<a href="/static/extra_files/example.pdf" alt="extra_files/example.pdf">extra_files/example.pdf<a>
</p>
</body>
</html>
]]></html>
</vertical>
<vertical display_name="Web Link Content" url_name="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
<html display_name="Web Link Content" url_name="resource_8_web_link_content"><![CDATA[<a href='http://web-link'>Web Link Content</a>]]></html>
<html display_name="Web Link Content" url_name="resource_8_web_link_content"><![CDATA[<a href="https://relative.source.domain/web-link">Web Link Content</a>]]></html>
</vertical>
</sequential>
</chapter>
Expand Down
Empty file.
191 changes: 191 additions & 0 deletions tests/test_content_parsers/test_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch

import pytest

from cc2olx.content_parsers import HtmlContentParser


class TestHtmlContentParser:
def test_parse_content_returns_default_content_if_there_is_no_resource_identifier(self):
parser = HtmlContentParser(Mock(), Mock())
expected_content = {"html": "<p>MISSING CONTENT</p>"}

actual_content = parser._parse_content(None)

assert actual_content == expected_content

def test_parse_content_returns_default_content_if_the_resource_is_missed_in_cartridge(self):
cartridge_mock = Mock(define_resource=Mock(return_value=None))
parser = HtmlContentParser(cartridge_mock, Mock())
expected_content = {"html": "<p>MISSING CONTENT</p>"}

actual_content = parser._parse_content(Mock())

assert actual_content == expected_content

@patch("cc2olx.content_parsers.html.logger")
def test_parse_content_logs_missing_resource(self, logger_mock):
cartridge_mock = Mock(define_resource=Mock(return_value=None))
parser = HtmlContentParser(cartridge_mock, Mock())
idref_mock = Mock()

parser._parse_content(idref_mock)

logger_mock.info.assert_called_once_with("Missing resource: %s", idref_mock)

@patch("cc2olx.content_parsers.html.HtmlContentParser._parse_web_link_content", Mock(return_value=None))
@patch("cc2olx.content_parsers.html.HtmlContentParser.is_known_unprocessed_resource_type", Mock(return_value=True))
def test_parse_content_returns_default_content_for_known_unprocessed_resource_types(self):
parser = HtmlContentParser(MagicMock(), Mock())
expected_content = {"html": "<p>MISSING CONTENT</p>"}

actual_content = parser._parse_content(Mock())

assert actual_content == expected_content

@pytest.mark.parametrize(
"resource_type",
[
"imsbasiclti_xmlv1p2",
"imsbasiclti_xmlv1p3",
"imsqti_xmlv1p3/imscc_xmlv1p1/assessment",
"imsqti_xmlv1p3/imscc_xmlv1p3/assessment",
"imsdt_xmlv1p2",
"imsdt_xmlv1p3",
],
)
def test_known_unprocessed_resource_types_is_detected(self, resource_type):
parser = HtmlContentParser(Mock(), Mock())

assert parser.is_known_unprocessed_resource_type(resource_type) is True

@pytest.mark.parametrize("resource_type", ["imsbasicabc_xmlv1p2", "imsexample_xmlv1p3", "not_cc_type", "imsscorm"])
def test_not_known_unprocessed_resource_types_is_detected(self, resource_type):
parser = HtmlContentParser(Mock(), Mock())

assert parser.is_known_unprocessed_resource_type(resource_type) is False

@pytest.mark.parametrize(
"resource_type",
["unsupported_resource_type", "chess_game_xmlv1p1", "drag_and_drop_xmlv1p1", "imsab_xmlv1p2"],
)
@patch("cc2olx.content_parsers.html.HtmlContentParser._parse_web_link_content", Mock(return_value=None))
@patch("cc2olx.content_parsers.html.HtmlContentParser._parse_not_imported_content")
def test_parse_content_parses_not_imported_content(self, parse_not_imported_content_mock, resource_type):
cartridge_mock = Mock(define_resource=Mock(return_value={"type": "imsqti_xmlv1p2"}))
parser = HtmlContentParser(cartridge_mock, Mock())

actual_content = parser._parse_content(Mock())

assert actual_content == parse_not_imported_content_mock.return_value

@patch("cc2olx.content_parsers.html.imghdr.what", Mock(return_value=None))
def test_parse_webcontent_returns_default_content_for_unknown_webcontent_type_from_web_resources_dir(self):
parser = HtmlContentParser(
Mock(build_resource_file_path=Mock(return_value=Path("web_resources/unknown/path/to/file.ext"))),
Mock(),
)
expected_content = {"html": "<p>MISSING CONTENT</p>"}

actual_content = parser._parse_webcontent(Mock(), MagicMock())

assert actual_content == expected_content

@patch("cc2olx.content_parsers.html.logger")
@patch("cc2olx.content_parsers.html.imghdr.what", Mock(return_value=None))
def test_parse_webcontent_logs_skipping_webcontent(self, logger_mock):
resource_file_path = Path("web_resources/unknown/path/to/file.ext")
parser = HtmlContentParser(Mock(build_resource_file_path=Mock(return_value=resource_file_path)), Mock())

parser._parse_webcontent(Mock(), MagicMock())

logger_mock.info.assert_called_once_with("Skipping webcontent: %s", resource_file_path)

@patch("cc2olx.content_parsers.html.logger")
@patch("cc2olx.content_parsers.html.open", Mock(side_effect=FileNotFoundError))
def test_webcontent_html_file_reading_failure_is_logged(self, logger_mock):
parser = HtmlContentParser(Mock(), Mock())
idref_mock = Mock()
resource_file_path_mock = Mock()

with pytest.raises(FileNotFoundError):
parser._parse_webcontent_html_file(idref_mock, resource_file_path_mock)

logger_mock.error.assert_called_once_with("Failure reading %s from id %s", resource_file_path_mock, idref_mock)

@pytest.mark.parametrize(
"resource,message",
[
(
{"type": "some_type_mock", "href": "https://example.com/some/type/link/"},
"Not imported content: type = 'some_type_mock', href = 'https://example.com/some/type/link/'",
),
({"type": "some_type_mock"}, "Not imported content: type = 'some_type_mock'"),
],
)
@patch("cc2olx.content_parsers.html.logger")
def test_not_imported_content_parsing_with_href_in_resource(self, logger_mock, resource, message):
parser = HtmlContentParser(Mock(), Mock())
expected_content = {"html": message}

actual_content = parser._parse_not_imported_content(resource)

logger_mock.info.assert_called_once_with("%s", message)
assert actual_content == expected_content

def test_parsing_results(self, cartridge):
parser = HtmlContentParser(cartridge, Mock())

assert parser.parse("resource_1_course") == {
"html": "Not imported content: type = 'associatedcontent/imscc_xmlv1p1/learning-application-resource', "
"href = 'course_settings/canvas_export.txt'"
}

assert parser.parse("resource_3_vertical") == {
"html": '<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
"<title>Vertical</title>\n"
'<meta name="identifier" content="resource_3_vertical"/>\n'
'<meta name="editing_roles" content="teachers"/>\n'
'<meta name="workflow_state" content="active"/>\n'
"</head>\n<body>\n"
'<img src="/static/QuizImages/fractal.jpg" alt="fractal.jpg"'
' width="500" height="375" />\n'
"<p>Fractal Image <a "
'href="/static/QuizImages/fractal.jpg?canvas_download=1" '
'target="_blank">Fractal Image</a></p>\n'
"</body>\n</html>\n"
}

assert parser.parse("resource_6_wiki_content") == {
"html": '<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
"<title>Vertical</title>\n"
'<meta name="identifier" content="resource_6_wiki_content"/>\n'
'<meta name="editing_roles" content="teachers"/>\n'
'<meta name="workflow_state" content="active"/>\n'
"</head>\n<body>\n"
'<p>Lorem ipsum...</p>\n<a href="/jump_to_id/resource_6_wiki_content">Wiki Content</a>'
"\n</body>\n</html>\n"
}

assert parser.parse("resource_7_canvas_content") == {
"html": '<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
"<title>Vertical</title>\n"
'<meta name="identifier" content="resource_7_canvas_content"/>\n'
'<meta name="editing_roles" content="teachers"/>\n'
'<meta name="workflow_state" content="active"/>\n'
"</head>\n<body>\n"
'<p>Lorem ipsum...</p>\n<a href="/jump_to_id/abc">Canvas Content</a>'
"\n</body>\n</html>\n"
}

assert parser.parse("resource_module-|-introduction") == {
"html": '<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
"<title>Vertical</title>\n"
'<meta name="identifier" content="resource_module-|-introduction"/>\n'
'<meta name="editing_roles" content="teachers"/>\n'
'<meta name="workflow_state" content="active"/>\n'
"</head>\n<body>\n"
'<p>Lorem ipsum...</p>\n<a href="/jump_to_id/resource_6_wiki_content">Wiki Content</a>'
"\n</body>\n</html>\n"
}
18 changes: 18 additions & 0 deletions tests/test_content_parsers/test_lti.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from unittest.mock import Mock

from cc2olx.content_parsers import LtiContentParser


class TestLtiContentParser:
def test_parsing_results(self, cartridge):
parser = LtiContentParser(cartridge, Mock())

assert parser.parse("resource_2_lti") == {
"title": "Learning Tools Interoperability",
"description": "https://www.imsglobal.org/activity/learning-tools-interoperability",
"launch_url": "https://lti.local/launch",
"height": "500",
"width": "500",
"custom_parameters": {},
"lti_id": "learning_tools_interoperability",
}
42 changes: 42 additions & 0 deletions tests/test_content_parsers/test_qti.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from unittest.mock import MagicMock, Mock, PropertyMock, call, patch

import pytest

from cc2olx.content_parsers import QtiContentParser
from cc2olx.exceptions import QtiError


class TestQtiContentParser:
@pytest.mark.parametrize("cc_profile", ["unknown_profile", "cc.chess.v0p1", "cc.drag_and_drop.v0p1", "123"])
def test_parse_problem_raises_qti_error_if_cc_profile_is_unknown(self, cc_profile):
parser = QtiContentParser(Mock(), Mock())
problem_mock = MagicMock(profile=cc_profile)

with pytest.raises(QtiError) as exc_info:
parser._parse_problem(problem_mock, Mock(), Mock())

assert str(exc_info.value) == f'Unknown cc_profile: "{cc_profile}"'

@patch("cc2olx.content_parsers.qti.logger")
def test_parse_problem_logs_inability_to_process_problem(self, logger_mock):
parser = QtiContentParser(Mock(), Mock())
ident_mock = MagicMock()
resource_file_path_mock = Mock()
cc_profile_mock = Mock()
problem_mock = Mock(profile=cc_profile_mock, attrib={"ident": ident_mock})
expected_logger_info_call_args_list = [
call("Problem with ID %s can't be converted.", ident_mock),
call(" Profile %s is not supported.", cc_profile_mock),
call(" At file %s.", resource_file_path_mock),
]

with patch(
"cc2olx.content_parsers.qti.QtiContentParser._problem_parsers_map",
new_callable=PropertyMock,
) as problem_parsers_map_mock:
problem_parsers_map_mock.return_value = {cc_profile_mock: Mock(side_effect=NotImplementedError)}

parser._parse_problem(problem_mock, Mock(), resource_file_path_mock)

assert logger_mock.info.call_count == 3
assert logger_mock.info.call_args_list == expected_logger_info_call_args_list
Loading

0 comments on commit 253b223

Please sign in to comment.