diff --git a/classes/cli_helper.php b/classes/cli_helper.php index 84e420e..c7ad19c 100644 --- a/classes/cli_helper.php +++ b/classes/cli_helper.php @@ -281,12 +281,13 @@ public function validate_and_clean_args(): void { break; } } - if (!(isset($cliargs['manifestpath']) || isset($cliargs['quizmanifestpath'])) && !isset($cliargs['contextlevel'])) { + if (!(isset($cliargs['manifestpath']) || isset($cliargs['quizmanifestpath']) + || (isset($cliargs['nonquizmanifestpath']) && isset($cliargs['instanceid']))) && !isset($cliargs['contextlevel'])) { echo "\nYou have not specified context. " . "You must specify context level (--contextlevel) unless " . "using a function where this information can be read from a manifest file, in which case " . "you could set a manifest path (--manifestpath) instead. If using exportrepofrommoodle, you " . - "must set manifest path only. If dealing with import/export of quizzes, you must specify --quizmanifestpath. " . + "must set manifest path only. If dealing with export of quizzes, you must specify --quizmanifestpath. " . "If you still see this message, you may be using invalid arguments.\n"; static::call_exit(); } diff --git a/classes/import_quiz.php b/classes/import_quiz.php index 292cc93..416d723 100644 --- a/classes/import_quiz.php +++ b/classes/import_quiz.php @@ -71,7 +71,7 @@ class import_quiz { * * @var string|null */ - public ?string $quizmanifestpath; + public ?string $quizmanifestpath = null; /** * Parsed content of JSON manifest file * @@ -83,7 +83,7 @@ class import_quiz { * * @var string|null */ - public ?string $nonquizmanifestpath; + public ?string $nonquizmanifestpath = null; /** * Parsed content of JSON manifest file * @@ -99,9 +99,9 @@ class import_quiz { /** * Full path to data file * - * @var string + * @var string|null */ - public string $quizdatapath; + public ?string $quizdatapath = null; /** * Parsed content of JSON data file * @@ -128,6 +128,17 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { if (!$this->quizmanifestcontents) { echo "\nUnable to access or parse manifest file: {$this->quizmanifestpath}\nAborting.\n"; $this->call_exit(); + + } else { + $this->quizdatapath = cli_helper::get_quiz_structure_path($this->quizmanifestcontents->context->modulename, dirname($this->quizmanifestpath)); + } + } else { + if ($arguments['quizdatapath']) { + $this->quizdatapath = $arguments['rootdirectory'] . '/' . $arguments['quizdatapath']; + } else { + echo "\nPlease supply a quiz manifest filepath or a quiz data filepath.\nAborting.\n"; + $this->call_exit(); + return; // Required for unit tests. } } if ($arguments['nonquizmanifestpath']) { @@ -138,9 +149,16 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { echo "\nUnable to access or parse manifest file: {$this->nonquizmanifestpath}\nAborting.\n"; $this->call_exit(); } - $instanceid = $this->quizmanifestcontents->context->instanceid; + if ($this->nonquizmanifestcontents->context->contextlevel === cli_helper::get_context_level('course')) { + $instanceid = $this->nonquizmanifestcontents->context->instanceid; + } + } + if (!$instanceid && !$arguments['coursename']) { + echo "\nYou must identify the course you wish to add the quiz to. Use a course manifest path (--nonquizmanifestpath)" . + "or specify the course id (--instanceid) or course name (--coursename).\nAborting.\n"; + $this->call_exit(); + return; // Required for unit tests. } - $this->quizdatapath = $arguments['rootdirectory'] . '/' . $arguments['quizdatapath']; $this->quizdatacontents = json_decode(file_get_contents($this->quizdatapath)); if (!$this->quizdatacontents) { echo "\nUnable to access or parse data file: {$this->quizdatapath}\nAborting.\n"; @@ -182,6 +200,18 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { ]; $this->curlrequest->set_option(CURLOPT_RETURNTRANSFER, true); $this->curlrequest->set_option(CURLOPT_POST, 1); + $instanceinfo = $this->clihelper->check_context($this, false, true); + if ($arguments['subcall']) { + echo "\nCreating quiz: {$this->quizdatacontents->quiz->name}\n"; + } else { + echo "\nPreparing to create a new quiz in Moodle.\n"; + echo "Moodle URL: {$this->moodleurl}\n"; + echo "Course: {$instanceinfo->contextinfo->coursename}\n"; + echo "Quiz: {$this->quizdatacontents->quiz->name}\n"; + $this->handle_abort(); + } + $this->postsettings['quiz[coursename]'] = $instanceinfo->contextinfo->coursename; + $this->postsettings['quiz[courseid]'] = $instanceinfo->contextinfo->instanceid; } /** @@ -211,17 +241,6 @@ public function get_curl_request($wsurl):curl_request { * @return void */ public function import_quiz_data() { - $instanceinfo = $this->clihelper->check_context($this, false, true); - $arguments = $this->clihelper->get_arguments(); - if ($arguments['subcall']) { - echo "Creating quiz: {$this->quizdatacontents->quiz->name}\n"; - } else { - echo "Preparing to create a new quiz in Moodle.\n"; - echo "Moodle URL: {$this->moodleurl}\n"; - echo "Course: {$instanceinfo->contextinfo->coursename}\n"; - echo "Quiz: {$this->quizdatacontents->quiz->name}\n"; - $this->handle_abort(); - } $quizmanifestentries = []; $nonquizmanifestentries = []; if ($this->quizmanifestpath) { @@ -234,8 +253,6 @@ public function import_quiz_data() { foreach ($this->quizdatacontents->quiz as $key => $quizparam) { $this->postsettings["quiz[{$key}]"] = $quizparam; } - $this->postsettings['quiz[coursename]'] = $instanceinfo->contextinfo->coursename; - $this->postsettings['quiz[courseid]'] = $instanceinfo->contextinfo->instanceid; foreach ($this->quizdatacontents->sections as $sectionkey => $section) { foreach ($section as $key => $sectionparam) { @@ -246,31 +263,31 @@ public function import_quiz_data() { foreach ($this->quizdatacontents->questions as $questionkey => $question) { foreach ($question as $key => $questionparam) { $this->postsettings["questions[{$questionkey}][{$key}]"] = $questionparam; - $manifestentry = false; - $qidentifier = ''; - if (isset($question->quizfilepath)) { - $manifestentry = $quizmanifestentries["{$question->quizfilepath}"] ?? false; - $qidentifier = "Quiz repo: {$question->quizfilepath}"; - unset($this->postsettings["questions[{$questionkey}][quizfilepath]"]); - } else if (isset($question->nonquizfilepath)) { - $manifestentry = $nonquizmanifestentries["{$question->nonquizfilepath}"] ?? false; - $qidentifier = "Non-quiz repo: {$question->nonquizfilepath}"; - unset($this->postsettings["questions[{$questionkey}][nonquizfilepath]"]); - } + } + $manifestentry = false; + $qidentifier = ''; + if (isset($question->quizfilepath)) { + $manifestentry = $quizmanifestentries["{$question->quizfilepath}"] ?? false; + $qidentifier = "Quiz repo: {$question->quizfilepath}"; + unset($this->postsettings["questions[{$questionkey}][quizfilepath]"]); + } else if (isset($question->nonquizfilepath)) { + $manifestentry = $nonquizmanifestentries["{$question->nonquizfilepath}"] ?? false; + $qidentifier = "Non-quiz repo: {$question->nonquizfilepath}"; + unset($this->postsettings["questions[{$questionkey}][nonquizfilepath]"]); + } - if ($manifestentry) { - $this->postsettings["questions[{$questionkey}][questionbankentryid]"] = $manifestentry->questionbankentryid; - } else { - $multiple = ($this->quizmanifestpath && $this->nonquizmanifestpath) ? 's' : ''; - echo "Question: {$qidentifier}\n"; - echo "This question is in the quiz but not in the supplied manifest file" . $multiple . ".\n"; - echo "Questions must either be in the repo for the quiz context defined by a supplied quiz manifest " . - "(--quizmanifestpath) or in the course context " . - "defined by a different manifest (--nonquizmanifestpath).\n"; - echo "You can supply either or both.\n"; - echo "Aborting.\n"; - $this->call_exit();; - } + if ($manifestentry) { + $this->postsettings["questions[{$questionkey}][questionbankentryid]"] = $manifestentry->questionbankentryid; + } else { + $multiple = ($this->quizmanifestpath && $this->nonquizmanifestpath) ? 's' : ''; + echo "Question: {$qidentifier}\n"; + echo "This question is in the quiz but not in the supplied manifest file" . $multiple . ".\n"; + echo "Questions must either be in the repo for the quiz context defined by a supplied quiz manifest " . + "(--quizmanifestpath) or in the course context " . + "defined by a different manifest (--nonquizmanifestpath).\n"; + echo "You can supply either or both.\n"; + echo "Aborting.\n"; + $this->call_exit(); } } diff --git a/testrepoparent/testrepo/fakeexportquiz_course_course-1_question_manifest.json b/testrepoparent/testrepo/fakeexportquiz_course_course-1_question_manifest.json index 5f58526..c40bcb1 100644 --- a/testrepoparent/testrepo/fakeexportquiz_course_course-1_question_manifest.json +++ b/testrepoparent/testrepo/fakeexportquiz_course_course-1_question_manifest.json @@ -1,6 +1,6 @@ { "context": { - "contextlevel": 10, + "contextlevel": 50, "coursename": "", "modulename": "", "coursecategory": "", diff --git a/testrepoparent/testrepo/fakeimportquiz_course_course-1_question_manifest.json b/testrepoparent/testrepo/fakeimportquiz_course_course-1_question_manifest.json new file mode 100644 index 0000000..6129dba --- /dev/null +++ b/testrepoparent/testrepo/fakeimportquiz_course_course-1_question_manifest.json @@ -0,0 +1,42 @@ +{ + "context": { + "contextlevel": 50, + "coursename": "", + "modulename": "", + "coursecategory": "", + "instanceid": "5", + "defaultsubdirectory": "top\/cat-2", + "defaultsubcategoryid": 5 + }, + "questions": [ + { + "questionbankentryid": "35001", + "filepath": "\/top\/cat-1\/First-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + }, + { + "questionbankentryid": "35002", + "filepath": "\/top\/cat-2\/subcat-2_1\/Third-Question.xml", + "importedversion": "6", + "exportedversion": "7", + "format": "xml" + }, + { + "questionbankentryid": "35004", + "filepath": "\/top\/cat-2\/subcat-2_1\/Fourth-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "currentcommit": "35004test", + "format": "xml" + }, + { + "questionbankentryid": "35003", + "filepath": "\/top\/cat-2\/Second-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + } + ] +} \ No newline at end of file diff --git a/testrepoparent/testrepo_quiz_quiz-1/fakeimportquiz_module_course-1_quiz-1_question_manifest.json b/testrepoparent/testrepo_quiz_quiz-1/fakeimportquiz_module_course-1_quiz-1_question_manifest.json new file mode 100644 index 0000000..4a12ca5 --- /dev/null +++ b/testrepoparent/testrepo_quiz_quiz-1/fakeimportquiz_module_course-1_quiz-1_question_manifest.json @@ -0,0 +1,27 @@ +{ + "context": { + "contextlevel": 70, + "coursename": "", + "modulename": "Import quiz", + "coursecategory": "", + "instanceid": "", + "defaultsubdirectory": "top", + "defaultsubcategoryid": 5 + }, + "questions": [ + { + "questionbankentryid": "36001", + "filepath": "\/top\/Quiz-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + }, + { + "questionbankentryid": "36002", + "filepath": "\/top\/quiz-cat\/Quiz-Question-2.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + } + ] +} \ No newline at end of file diff --git a/testrepoparent/testrepo_quiz_quiz-1/import-quiz_quiz.json b/testrepoparent/testrepo_quiz_quiz-1/import-quiz_quiz.json new file mode 100644 index 0000000..322f573 --- /dev/null +++ b/testrepoparent/testrepo_quiz_quiz-1/import-quiz_quiz.json @@ -0,0 +1,39 @@ +{ + "quiz": { + "name": "Quiz 1", + "intro": "Quiz intro", + "introformat": "0", + "questionsperpage": "0", + "grade": "100.00000", + "navmethod": "free" + }, + "sections": [ + { + "firstslot": "1", + "heading": "Heading 1", + "shufflequestions": 0 + }, + { + "firstslot": "2", + "heading": "Heading 2", + "shufflequestions": 0 + } + ], + "questions": [ + { + "quizfilepath": "\/top\/Quiz-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ], + "feedback": [ + { + "feedbacktext": "Quiz feedback", + "feedbacktextformat": "0", + "mingrade": "0.0000000", + "maxgrade": "50.000000" + } + ] +} \ No newline at end of file diff --git a/tests/export_quiz_test.php b/tests/export_quiz_test.php index 0954288..68277f5 100644 --- a/tests/export_quiz_test.php +++ b/tests/export_quiz_test.php @@ -15,7 +15,7 @@ // along with Stack. If not, see . /** - * Unit tests for export repo command line script for gitsync + * Unit tests for export quiz command line script for gitsync * * @package qbank_gitsync * @copyright 2023 The Open University @@ -75,7 +75,7 @@ class export_quiz_test extends advanced_testcase { /** @var string root of virtual file system */ public string $rootpath; /** @var string used to store output of multiple calls to a function */ - const MOODLE = 'fakeexportquiz'; /** Name of question to be generated and exported. */ + const MOODLE = 'fakeexportquiz'; const QUIZNAME = 'Quiz 1'; const QUIZINTRO = 'Quiz intro'; const FEEDBACK = 'Quiz feedback'; @@ -179,7 +179,7 @@ public function set_up_mocks() { /** * Test the full process. */ - public function x_test_process(): void { + public function test_process(): void { $this->set_up_mocks(); $this->curl->expects($this->exactly(1))->method('execute')->willReturnOnConsecutiveCalls( json_encode($this->quizoutput) @@ -198,7 +198,7 @@ public function x_test_process(): void { /** * Test message if export JSON broken. */ - public function x_test_broken_json_on_export(): void { + public function test_broken_json_on_export(): void { $this->set_up_mocks(); $this->curl->expects($this->any())->method('execute')->willReturn( '{"quiz": "}' @@ -213,7 +213,7 @@ public function x_test_broken_json_on_export(): void { /** * Test message if export exception. */ - public function x_test_exception_on_export(): void { + public function test_exception_on_export(): void { $this->set_up_mocks(); $this->curl->expects($this->any())->method('execute')->willReturn( '{"exception":"moodle_exception","message":"No token"}' @@ -227,7 +227,7 @@ public function x_test_exception_on_export(): void { /** * Test message if manifest file update issue. */ - public function x_test_manifest_file_update_error(): void { + public function test_manifest_file_update_error(): void { $this->set_up_mocks(); $this->curl->expects($this->any())->method('execute')->willReturn( json_encode($this->quizoutput) @@ -243,7 +243,7 @@ public function x_test_manifest_file_update_error(): void { /** * Test if quiz context questions. */ - public function x_test_quiz_context_questions(): void { + public function test_quiz_context_questions(): void { $this->quizoutput['questions'][] = [ 'questionbankentryid' => '36002', @@ -271,7 +271,7 @@ public function x_test_quiz_context_questions(): void { /** * Test if course context questions. */ - public function x_test_course_context_questions(): void { + public function test_course_context_questions(): void { $this->quizoutput['questions'] = [ [ 'questionbankentryid' => '35002', @@ -307,7 +307,7 @@ public function x_test_course_context_questions(): void { /** * Test if missing questions. */ - public function x_test_missing_questions(): void { + public function test_missing_questions(): void { $this->quizoutput['questions'] = [ [ 'questionbankentryid' => '35002', diff --git a/tests/import_quiz_test.php b/tests/import_quiz_test.php new file mode 100644 index 0000000..03f1b38 --- /dev/null +++ b/tests/import_quiz_test.php @@ -0,0 +1,572 @@ +. + +/** + * Unit tests for import quiz command line script for gitsync + * + * @package qbank_gitsync + * @copyright 2024 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_gitsync; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +use advanced_testcase; +use org\bovigo\vfs\vfsStream; + +/** + * Allows testing of errors that lead to an exit. + */ +class fake_import_cli_helper extends cli_helper { + /** + * Override so ignored during testing + * + * @return void + */ + public static function call_exit():void { + return; + } + + /** + * Override so ignored during testing + * + * @return void + */ + public static function handle_abort():void { + return; + } +} + + +/** + * Test the CLI script for importing a repo from Moodle. + * @group qbank_gitsync + * + * @covers \gitsync\import_repo::class + */ +class import_quiz_test extends advanced_testcase { + /** @var array mocked output of cli_helper->get_arguments */ + public array $options; + /** @var array of instance names and URLs */ + public array $moodleinstances; + /** @var cli_helper mocked cli_helper */ + public cli_helper $clihelper; + /** @var curl_request mocked curl_request */ + public curl_request $curl; + /** @var import_quiz mocked import_quiz */ + public import_quiz $importquiz; + /** @var curl_request mocked curl_request for question list */ + public curl_request $listcurl; + /** @var string root of virtual file system */ + public string $rootpath; + /** @var string used to store output of multiple calls to a function */ + const MOODLE = 'fakeimportquiz'; + const QUIZNAME = 'Quiz 1'; + const QUIZINTRO = 'Quiz intro'; + const FEEDBACK = 'Quiz feedback'; + const HEADING1 = 'Heading 1'; + const HEADING2 = 'Heading 2'; + const COURSENAME = 'Course 1'; + protected array $quizoutput = [ + "wstoken" => "XXXXXX", + "wsfunction" => "qbank_gitsync_import_quiz_data", + "moodlewsrestformat" => "json", + "quiz[coursename]" => "Course 1", + "quiz[courseid]" => "5", + "quiz[name]" => "Quiz 1", + "quiz[intro]" => "Quiz intro", + "quiz[introformat]" => "0", + "quiz[questionsperpage]" => "0", + "quiz[grade]" => "100.00000", + "quiz[navmethod]" => "free", + "sections[0][firstslot]" => "1", + "sections[0][heading]" => "Heading 1", + "sections[0][shufflequestions]" => 0, + "sections[1][firstslot]" => "2", + "sections[1][heading]" => "Heading 2", + "sections[1][shufflequestions]" => 0, + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "36001", + "feedback[0][feedbacktext]" => "Quiz feedback", + "feedback[0][feedbacktextformat]" => "0", + "feedback[0][mingrade]" => "0.0000000", + "feedback[0][maxgrade]" => "50.000000" + ]; + + public function setUp(): void { + global $CFG; + $this->moodleinstances = [self::MOODLE => 'fakeurl.com']; + // Copy test repo to virtual file stream. + $root = vfsStream::setup(); + vfsStream::copyFromFileSystem($CFG->dirroot . '/question/bank/gitsync/testrepoparent/', $root); + $this->rootpath = vfsStream::url('root'); + + // Mock the combined output of command line options and defaults. + $this->options = [ + 'moodleinstance' => self::MOODLE, + 'rootdirectory' => $this->rootpath, + 'nonquizmanifestpath' => '/testrepo/' . self::MOODLE . '_course_course-1' . cli_helper::MANIFEST_FILE, + 'quizmanifestpath' => '/testrepo_quiz_quiz-1/' . self::MOODLE . '_module_course-1_quiz-1' . cli_helper::MANIFEST_FILE, + 'quizdatapath' => null, + 'coursename' => null, + 'instanceid' => null, + 'token' => 'XXXXXX', + 'help' => false, + 'subcall' => false, + ]; + + } + + /** + * Mock set up + * + * @return void + */ + public function set_up_mocks() { + $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ + 'get_arguments', 'check_context', + ])->setConstructorArgs([[]])->getMock(); + $this->clihelper->expects($this->any())->method('get_arguments')->will($this->returnValue($this->options)); + $this->clihelper->expects($this->any())->method('check_context')->willReturn( + json_decode('{"contextinfo":{"contextlevel": "module", "categoryname":"", "coursename":"Course 1", + "modulename":"Module 1", "instanceid":"5", "qcategoryname":"top"}, + "questions": []}') + ); + // Mock call to webservice. + $this->curl = $this->getMockBuilder(\qbank_gitsync\curl_request::class)->onlyMethods([ + 'execute', + ])->setConstructorArgs(['xxxx'])->getMock(); + $this->listcurl = $this->getMockBuilder(\qbank_gitsync\curl_request::class)->onlyMethods([ + 'execute', + ])->setConstructorArgs(['xxxx'])->getMock(); + $this->importquiz = $this->getMockBuilder(\qbank_gitsync\import_quiz::class)->onlyMethods([ + 'get_curl_request', 'call_exit', 'handle_abort', + ])->setConstructorArgs([$this->clihelper, $this->moodleinstances])->getMock(); + $this->importquiz->curlrequest = $this->curl; + $this->importquiz->listcurlrequest = $this->listcurl; + } + + /** + * Test the full process. + */ + public function test_process(): void { + $this->set_up_mocks(); + $this->curl->expects($this->exactly(1))->method('execute')->willReturnOnConsecutiveCalls( + '{"success": true}' + ); + $this->importquiz->process(); + $this->assertEquals(json_encode($this->quizoutput), json_encode($this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } + + /** + * Test message if import JSON broken. + */ + public function test_broken_json_on_import(): void { + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + '{"quiz": "}' + ); + + $this->importquiz->process(); + + $this->expectOutputRegex('/Broken JSON returned from Moodle:' . + '.*{"quiz": <\/Question>"}/s'); + } + + /** + * Test message if import exception. + */ + public function test_exception_on_import(): void { + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + '{"exception":"moodle_exception","message":"No token"}' + ); + + $this->importquiz->process(); + + $this->expectOutputRegex('/No token/'); + } + + /** + * Test message if manifest file open issue. + */ + public function test_manifest_file_open_error(): void { + $this->set_up_mocks(); + chmod($this->importquiz->quizmanifestpath, 0000); + @$this->importquiz->__construct($this->clihelper, $this->moodleinstances); + $this->expectOutputRegex('/.*Unable to access or parse manifest file:.*testrepo_quiz_quiz-1\/fakeimportquiz_module_course-1_quiz-1_question_manifest.json.*Aborting.*$/s'); + } + + /** + * Test message if manifest file open issue. + */ + public function test_nonquiz_manifest_file_open_error(): void { + $this->set_up_mocks(); + chmod($this->importquiz->nonquizmanifestpath, 0000); + @$this->importquiz->__construct($this->clihelper, $this->moodleinstances); + $this->expectOutputRegex('/.*Unable to access or parse manifest file:.*fakeimportquiz_course_course-1_question_manifest.json.*Aborting.*$/s'); + } + + /** + * Test message if data file open issue. + */ + public function test_data_file_open_error(): void { + $this->options['quizdatapath'] = '/testrepo_quiz_quiz-1/' . 'import-quiz' . cli_helper::QUIZ_FILE; + $this->set_up_mocks(); + chmod($this->importquiz->quizdatapath, 0000); + @$this->importquiz->__construct($this->clihelper, $this->moodleinstances); + $this->expectOutputRegex('/.*Unable to access or parse data file:.*testrepo_quiz_quiz-1\/import-quiz_quiz.json.*Aborting.*$/s'); + } + + /** + * Test validation of supplied datapath info. + */ + public function test_no_data_info(): void { + $this->options['quizmanifestpath'] = null; + $this->set_up_mocks(); + $this->expectOutputRegex('/^\nPlease supply a quiz manifest filepath or a quiz data filepath.*Aborting.\n$/s'); + } + + /** + * Test validation of supplied course info. + */ + public function test_no_course_info(): void { + $this->options['nonquizmanifestpath'] = null; + $this->set_up_mocks(); + $this->expectOutputRegex('/^\nYou must identify the course you wish to add the quiz to.*Aborting.\n$/s'); + } + + /** + * Test if quiz context questions. + */ + public function test_quiz_context_questions(): void { + $questions = '[ + { + "quizfilepath": "\/top\/Quiz-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "quizfilepath": "\/top\/quiz-cat\/Quiz-Question-2.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "36001", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "36002" + ]; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->quizoutput = array_merge($this->quizoutput, $output); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->assertEquals([], array_diff_assoc($this->quizoutput, $this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } + + /** + * Test if quiz context questions with no course. + */ + public function test_quiz_context_questions_no_course(): void { + $questions = '[ + { + "quizfilepath": "\/top\/Quiz-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "quizfilepath": "\/top\/quiz-cat\/Quiz-Question-2.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "36001", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "36002" + ]; + $this->options['nonquizmanifestpath'] = null; + $this->set_up_mocks(); + $this->expectOutputRegex('/^\nYou must identify the course you wish to add the quiz to.*Aborting.\n$/s'); + } + + /** + * Test if quiz context questions with no course manifest. + */ + public function test_quiz_context_questions_no_course_file(): void { + $questions = '[ + { + "quizfilepath": "\/top\/Quiz-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "quizfilepath": "\/top\/quiz-cat\/Quiz-Question-2.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "36001", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "36002" + ]; + $this->options['nonquizmanifestpath'] = null; + $this->options['coursename'] = 'Course 1'; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->quizoutput = array_merge($this->quizoutput, $output); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->assertEquals([], array_diff_assoc($this->quizoutput, $this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } + + /** + * Test if course context questions. + */ + public function test_course_context_questions(): void { + $questions = '[ + { + "nonquizfilepath": "\/top\/cat-2\/subcat-2_1\/Fourth-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "nonquizfilepath": "\/top\/cat-1\/First-Question.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "35004", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "35001" + ]; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->quizoutput = array_merge($this->quizoutput, $output); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->assertEquals([], array_diff_assoc($this->quizoutput, $this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } + + /** + * Test if course context questions but no quiz manifest. + */ + public function test_course_context_questions_no_quiz_manifest(): void { + $questions = '[ + { + "nonquizfilepath": "\/top\/cat-2\/subcat-2_1\/Fourth-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "nonquizfilepath": "\/top\/cat-1\/First-Question.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "35004", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "35001" + ]; + $this->options['quizmanifestpath'] = null; + $this->options['quizdatapath'] = '/testrepo_quiz_quiz-1/' . 'import-quiz' . cli_helper::QUIZ_FILE; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->quizoutput = array_merge($this->quizoutput, $output); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->assertEquals([], array_diff_assoc($this->quizoutput, $this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } + + /** + * Test if missing questions. + */ + public function test_missing_questions(): void { + $questions = '[ + { + "nonquizfilepath": "\/top\/cat-2\/subcat-2_1\/Fake-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "nonquizfilepath": "\/top\/cat-1\/First-Question.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->expectOutputRegex('/.*Question: Non-quiz repo: \/top\/cat-2\/subcat-2_1\/Fake-Question.xml\nThis question is in the quiz but not in the supplied manifest file.*/s'); + } + + /** + * Test if mixed context questions. + */ + public function test_mixed_context_questions(): void { + $questions = '[ + { + "quizfilepath": "\/top\/Quiz-Question.xml", + "slot": "1", + "page": "1", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "quizfilepath": "\/top\/quiz-cat\/Quiz-Question-2.xml", + "slot": "2", + "page": "2", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "nonquizfilepath": "\/top\/cat-2\/subcat-2_1\/Fourth-Question.xml", + "slot": "3", + "page": "3", + "requireprevious": 0, + "maxmark": "1.0000000" + }, + { + "nonquizfilepath": "\/top\/cat-1\/First-Question.xml", + "slot": "4", + "page": "4", + "requireprevious": 0, + "maxmark": "1.0000000" + } + ]'; + $output = [ + "questions[0][slot]" => "1", + "questions[0][page]" => "1", + "questions[0][requireprevious]" => 0, + "questions[0][maxmark]" => "1.0000000", + "questions[0][questionbankentryid]" => "36001", + "questions[1][slot]" => "2", + "questions[1][page]" => "2", + "questions[1][requireprevious]" => 0, + "questions[1][maxmark]" => "1.0000000", + "questions[1][questionbankentryid]" => "36002", + "questions[2][slot]" => "3", + "questions[2][page]" => "3", + "questions[2][requireprevious]" => 0, + "questions[2][maxmark]" => "1.0000000", + "questions[2][questionbankentryid]" => "35004", + "questions[3][slot]" => "4", + "questions[3][page]" => "4", + "questions[3][requireprevious]" => 0, + "questions[3][maxmark]" => "1.0000000", + "questions[3][questionbankentryid]" => "35001" + ]; + $this->set_up_mocks(); + $this->curl->expects($this->any())->method('execute')->willReturn( + json_encode($this->quizoutput) + ); + $questions = json_decode($questions); + $this->quizoutput = array_merge($this->quizoutput, $output); + $this->importquiz->quizdatacontents->questions = $questions; + $this->importquiz->process(); + $this->assertEquals([], array_diff_assoc($this->quizoutput, $this->importquiz->postsettings)); + $this->expectOutputRegex('/Quiz imported.\n$/s'); + } +}