. /** * Unit tests for the calendar event modification callbacks used * for dragging and dropping quiz calendar events in the calendar * UI. * * @package mod_quiz * @category test * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/quiz/lib.php'); /** * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class mod_quiz_calendar_event_modified_testcase extends advanced_testcase { /** * Create an instance of the quiz activity. * * @param array $properties Properties to set on the activity * @return stdClass Quiz activity instance */ protected function create_quiz_instance(array $properties) { global $DB; $generator = $this->getDataGenerator(); if (empty($properties['course'])) { $course = $generator->create_course(); $courseid = $course->id; } else { $courseid = $properties['course']; } $quizgenerator = $generator->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance(array_merge(['course' => $courseid], $properties)); if (isset($properties['timemodified'])) { // The generator overrides the timemodified value to set it as // the current time even if a value is provided so we need to // make sure it's set back to the requested value. $quiz->timemodified = $properties['timemodified']; $DB->update_record('quiz', $quiz); } return $quiz; } /** * Create a calendar event for a quiz activity instance. * * @param stdClass $quiz The activity instance * @param array $eventproperties Properties to set on the calendar event * @return calendar_event */ protected function create_quiz_calendar_event(\stdClass $quiz, array $eventproperties) { $defaultproperties = [ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $quiz->course, 'groupid' => 0, 'userid' => 2, 'modulename' => 'quiz', 'instance' => $quiz->id, 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ]; return new \calendar_event(array_merge($defaultproperties, $eventproperties)); } /** * An unkown event type should not change the quiz instance. */ public function test_mod_quiz_core_calendar_event_timestart_updated_unknown_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $quiz = $this->create_quiz_instance(['timeopen' => $timeopen, 'timeclose' => $timeclose]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE", 'timestart' => 1 ]); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); $this->assertEquals($timeopen, $quiz->timeopen); $this->assertEquals($timeclose, $quiz->timeclose); } /** * A QUIZ_EVENT_TYPE_OPEN event should update the timeopen property of * the quiz activity. */ public function test_mod_quiz_core_calendar_event_timestart_updated_open_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeopen = $timeopen - DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose, 'timemodified' => $timemodified ]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => $newtimeopen ]); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); // Ensure the timeopen property matches the event timestart. $this->assertEquals($newtimeopen, $quiz->timeopen); // Ensure the timeclose isn't changed. $this->assertEquals($timeclose, $quiz->timeclose); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $quiz->timemodified); } /** * A QUIZ_EVENT_TYPE_CLOSE event should update the timeclose property of * the quiz activity. */ public function test_mod_quiz_core_calendar_event_timestart_updated_close_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeclose = $timeclose + DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose, 'timemodified' => $timemodified ]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose ]); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); // Ensure the timeclose property matches the event timestart. $this->assertEquals($newtimeclose, $quiz->timeclose); // Ensure the timeopen isn't changed. $this->assertEquals($timeopen, $quiz->timeopen); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $quiz->timemodified); } /** * A QUIZ_EVENT_TYPE_OPEN event should not update the timeopen property of * the quiz activity if it's an override. */ public function test_mod_quiz_core_calendar_event_timestart_updated_open_event_override() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeopen = $timeopen - DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose, 'timemodified' => $timemodified ]); $event = $this->create_quiz_calendar_event($quiz, [ 'userid' => $user->id, 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => $newtimeopen ]); $record = (object) [ 'quiz' => $quiz->id, 'userid' => $user->id ]; $DB->insert_record('quiz_overrides', $record); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); // Ensure the timeopen property doesn't change. $this->assertEquals($timeopen, $quiz->timeopen); // Ensure the timeclose isn't changed. $this->assertEquals($timeclose, $quiz->timeclose); // Ensure the timemodified property has not been changed. $this->assertEquals($timemodified, $quiz->timemodified); } /** * If a student somehow finds a way to update the quiz calendar event * then the callback should not update the quiz activity otherwise that * would be a security issue. */ public function test_student_role_cant_update_quiz_activity() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = context_course::instance($course->id); $roleid = $generator->create_role(); $now = time(); $timeopen = (new DateTime())->setTimestamp($now); $newtimeopen = (new DateTime())->setTimestamp($now)->modify('+1 day'); $quiz = $this->create_quiz_instance([ 'course' => $course->id, 'timeopen' => $timeopen->getTimestamp() ]); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => $timeopen->getTimestamp() ]); assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true); $this->setUser($user); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]); // The time open shouldn't have changed even though we updated the calendar // event. $this->assertEquals($timeopen->getTimestamp(), $newquiz->timeopen); } /** * A teacher with the capability to modify a quiz module should be * able to update the quiz activity dates by changing the calendar * event. */ public function test_teacher_role_can_update_quiz_activity() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = context_course::instance($course->id); $roleid = $generator->create_role(); $now = time(); $timeopen = (new DateTime())->setTimestamp($now); $newtimeopen = (new DateTime())->setTimestamp($now)->modify('+1 day'); $quiz = $this->create_quiz_instance([ 'course' => $course->id, 'timeopen' => $timeopen->getTimestamp() ]); $generator->enrol_user($user->id, $course->id, 'teacher'); $generator->role_assign($roleid, $user->id, $context->id); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => $newtimeopen->getTimestamp() ]); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); // Trigger and capture the event. $sink = $this->redirectEvents(); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $triggeredevents = $sink->get_events(); $moduleupdatedevents = array_filter($triggeredevents, function($e) { return is_a($e, 'core\event\course_module_updated'); }); $newquiz = $DB->get_record('quiz', ['id' => $quiz->id]); // The should be updated along with the event because the user has sufficient // capabilities. $this->assertEquals($newtimeopen->getTimestamp(), $newquiz->timeopen); // Confirm that a module updated event is fired when the module // is changed. $this->assertNotEmpty($moduleupdatedevents); } /** * An unkown event type should not have any limits */ public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_unknown_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose ]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN . "SOMETHING ELSE", 'timestart' => 1 ]); list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertNull($min); $this->assertNull($max); } /** * The open event should be limited by the quiz's timeclose property, if it's set. */ public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_open_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose ]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => 1 ]); // The max limit should be bounded by the timeclose value. list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertNull($min); $this->assertEquals($timeclose, $max[0]); // No timeclose value should result in no upper limit. $quiz->timeclose = 0; list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertNull($min); $this->assertNull($max); } /** * An override event should not have any limits. */ public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_override_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $quiz = $this->create_quiz_instance([ 'course' => $course->id, 'timeopen' => $timeopen, 'timeclose' => $timeclose ]); $event = $this->create_quiz_calendar_event($quiz, [ 'userid' => $user->id, 'eventtype' => QUIZ_EVENT_TYPE_OPEN, 'timestart' => 1 ]); $record = (object) [ 'quiz' => $quiz->id, 'userid' => $user->id ]; $DB->insert_record('quiz_overrides', $record); list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertFalse($min); $this->assertFalse($max); } /** * The close event should be limited by the quiz's timeopen property, if it's set. */ public function test_mod_quiz_core_calendar_get_valid_event_timestart_range_close_event() { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $quiz = $this->create_quiz_instance([ 'timeopen' => $timeopen, 'timeclose' => $timeclose ]); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, 'timestart' => 1, ]); // The max limit should be bounded by the timeclose value. list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertEquals($timeopen, $min[0]); $this->assertNull($max); // No timeclose value should result in no upper limit. $quiz->timeopen = 0; list ($min, $max) = mod_quiz_core_calendar_get_valid_event_timestart_range($event, $quiz); $this->assertNull($min); $this->assertNull($max); } /** * When the close date event is changed and it results in the time close value of * the quiz being updated then the open quiz attempts should also be updated. */ public function test_core_calendar_event_timestart_updated_update_quiz_attempt() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $teacher = $generator->create_user(); $student = $generator->create_user(); $course = $generator->create_course(); $context = context_course::instance($course->id); $roleid = $generator->create_role(); $now = time(); $timelimit = 600; $timeopen = (new DateTime())->setTimestamp($now); $timeclose = (new DateTime())->setTimestamp($now)->modify('+1 day'); // The new close time being earlier than the time open + time limit should // result in an update to the quiz attempts. $newtimeclose = $timeopen->getTimestamp() + $timelimit - 10; $quiz = $this->create_quiz_instance([ 'course' => $course->id, 'timeopen' => $timeopen->getTimestamp(), 'timeclose' => $timeclose->getTimestamp(), 'timelimit' => $timelimit ]); $generator->enrol_user($student->id, $course->id, 'student'); $generator->enrol_user($teacher->id, $course->id, 'teacher'); $generator->role_assign($roleid, $teacher->id, $context->id); $event = $this->create_quiz_calendar_event($quiz, [ 'eventtype' => QUIZ_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose ]); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); $attemptid = $DB->insert_record( 'quiz_attempts', [ 'quiz' => $quiz->id, 'userid' => $student->id, 'state' => 'inprogress', 'timestart' => $timeopen->getTimestamp(), 'timecheckstate' => 0, 'layout' => '', 'uniqueid' => 1 ] ); $this->setUser($teacher); mod_quiz_core_calendar_event_timestart_updated($event, $quiz); $quiz = $DB->get_record('quiz', ['id' => $quiz->id]); $attempt = $DB->get_record('quiz_attempts', ['id' => $attemptid]); // When the close date is changed so that it's earlier than the time open // plus the time limit of the quiz then the attempt's timecheckstate should // be updated to the new time close date of the quiz. $this->assertEquals($newtimeclose, $attempt->timecheckstate); $this->assertEquals($newtimeclose, $quiz->timeclose); } }