. /** * Privacy provider tests. * * @package mod_quiz * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_privacy\local\metadata\collection; use core_privacy\local\request\deletion_criteria; use core_privacy\local\request\writer; use mod_quiz\privacy\provider; use mod_quiz\privacy\helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/question/tests/privacy_helper.php'); /** * Privacy provider tests class. * * @package mod_quiz * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_quiz_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { use core_question_privacy_helper; /** * Test that a user who has no data gets no contexts */ public function test_get_contexts_for_userid_no_data() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $contextlist = provider::get_contexts_for_userid($USER->id); $this->assertEmpty($contextlist); } /** * Test for provider::get_contexts_for_userid() when there is no quiz attempt at all. */ public function test_get_contexts_for_userid_no_attempt_with_override() { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); // Make a quiz with an override. $this->setUser(); $quiz = $this->create_test_quiz($course); $DB->insert_record('quiz_overrides', [ 'quiz' => $quiz->id, 'userid' => $user->id, 'timeclose' => 1300, 'timelimit' => null, ]); $cm = get_coursemodule_from_instance('quiz', $quiz->id); $context = \context_module::instance($cm->id); // Fetch the contexts - only one context should be returned. $this->setUser(); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); } /** * The export function should handle an empty contextlist properly. */ public function test_export_user_data_no_data() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($USER->id), 'mod_quiz', [] ); provider::export_user_data($approvedcontextlist); $this->assertDebuggingNotCalled(); // No data should have been exported. $writer = \core_privacy\local\request\writer::with_context(\context_system::instance()); $this->assertFalse($writer->has_any_data_in_any_context()); } /** * The delete function should handle an empty contextlist properly. */ public function test_delete_data_for_user_no_data() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($USER->id), 'mod_quiz', [] ); provider::delete_data_for_user($approvedcontextlist); $this->assertDebuggingNotCalled(); } /** * Export + Delete quiz data for a user who has made a single attempt. */ public function test_user_with_data() { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); // Make a quiz with an override. $this->setUser(); $quiz = $this->create_test_quiz($course); $DB->insert_record('quiz_overrides', [ 'quiz' => $quiz->id, 'userid' => $user->id, 'timeclose' => 1300, 'timelimit' => null, ]); // Run as the user and make an attempt on the quiz. list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user); $this->attempt_quiz($quiz, $otheruser); $context = $quizobj->get_context(); // Fetch the contexts - only one context should be returned. $this->setUser(); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); $this->assertEquals($context, $contextlist->current()); // Perform the export and check the data. $this->setUser($user); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_quiz', $contextlist->get_contextids() ); provider::export_user_data($approvedcontextlist); // Ensure that the quiz data was exported correctly. $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $quizdata = $writer->get_data([]); $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name); // Every module has an intro. $this->assertTrue(isset($quizdata->intro)); // Fetch the attempt data. $attempt = $attemptobj->get_attempt(); $attemptsubcontext = [ get_string('attempts', 'mod_quiz'), $attempt->attempt, ]; $attemptdata = writer::with_context($context)->get_data($attemptsubcontext); $attempt = $attemptobj->get_attempt(); $this->assertTrue(isset($attemptdata->state)); $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state); $this->assertTrue(isset($attemptdata->timestart)); $this->assertTrue(isset($attemptdata->timefinish)); $this->assertTrue(isset($attemptdata->timemodified)); $this->assertFalse(isset($attemptdata->timemodifiedoffline)); $this->assertFalse(isset($attemptdata->timecheckstate)); $this->assertTrue(isset($attemptdata->grade)); $this->assertEquals(100.00, $attemptdata->grade->grade); // Check that the exported question attempts are correct. $attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user); $this->assert_question_attempt_exported( $context, $attemptsubcontext, \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()), quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context), $user ); // Delete the data and check it is removed. $this->setUser(); provider::delete_data_for_user($approvedcontextlist); $this->expectException(\dml_missing_record_exception::class); \quiz_attempt::create($attemptobj->get_quizid()); } /** * Export + Delete quiz data for a user who has made a single attempt. */ public function test_user_with_preview() { global $DB; $this->resetAfterTest(true); // Make a quiz. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance([ 'course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Create a couple of questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); quiz_add_quiz_question($saq->id, $quiz); $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); quiz_add_quiz_question($numq->id, $quiz); // Run as the user and make an attempt on the quiz. $this->setUser($user); $starttime = time(); $quizobj = quiz::create($quiz->id, $user->id); $context = $quizobj->get_context(); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // Start the attempt. $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], 2 => ['answer' => '3.14'], ]; $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. $attemptobj = quiz_attempt::create($attempt->id); $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); $attemptobj->process_finish($starttime, false); // Fetch the contexts - no context should be returned. $this->setUser(); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(0, $contextlist); } /** * Export + Delete quiz data for a user who has made a single attempt. */ public function test_delete_data_for_all_users_in_context() { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); // Make a quiz with an override. $this->setUser(); $quiz = $this->create_test_quiz($course); $DB->insert_record('quiz_overrides', [ 'quiz' => $quiz->id, 'userid' => $user->id, 'timeclose' => 1300, 'timelimit' => null, ]); // Run as the user and make an attempt on the quiz. list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user); list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $otheruser); // Create another quiz and questions, and repeat the data insertion. $this->setUser(); $otherquiz = $this->create_test_quiz($course); $DB->insert_record('quiz_overrides', [ 'quiz' => $otherquiz->id, 'userid' => $user->id, 'timeclose' => 1300, 'timelimit' => null, ]); // Run as the user and make an attempt on the quiz. list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $user); list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $otheruser); // Delete all data for all users in the context under test. $this->setUser(); $context = $quizobj->get_context(); provider::delete_data_for_all_users_in_context($context); // The quiz attempt should have been deleted from this quiz. $this->assertCount(0, $DB->get_records('quiz_attempts', ['quiz' => $quizobj->get_quizid()])); $this->assertCount(0, $DB->get_records('quiz_overrides', ['quiz' => $quizobj->get_quizid()])); $this->assertCount(0, $DB->get_records('question_attempts', ['questionusageid' => $quba->get_id()])); // But not for the other quiz. $this->assertNotCount(0, $DB->get_records('quiz_attempts', ['quiz' => $otherquizobj->get_quizid()])); $this->assertNotCount(0, $DB->get_records('quiz_overrides', ['quiz' => $otherquizobj->get_quizid()])); $this->assertNotCount(0, $DB->get_records('question_attempts', ['questionusageid' => $otherquba->get_id()])); } /** * Export + Delete quiz data for a user who has made a single attempt. */ public function test_wrong_context() { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); // Make a choice. $this->setUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice'); $choice = $plugingenerator->create_instance(['course' => $course->id]); $cm = get_coursemodule_from_instance('choice', $choice->id); $context = \context_module::instance($cm->id); // Fetch the contexts - no context should be returned. $this->setUser(); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(0, $contextlist); // Perform the export and check the data. $this->setUser($user); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_quiz', [$context->id] ); provider::export_user_data($approvedcontextlist); // Ensure that nothing was exported. $writer = writer::with_context($context); $this->assertFalse($writer->has_any_data_in_any_context()); $this->setUser(); $dbwrites = $DB->perf_get_writes(); // Perform a deletion with the approved contextlist containing an incorrect context. $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( \core_user::get_user($user->id), 'mod_quiz', [$context->id] ); provider::delete_data_for_user($approvedcontextlist); $this->assertEquals($dbwrites, $DB->perf_get_writes()); $this->assertDebuggingNotCalled(); // Perform a deletion of all data in the context. provider::delete_data_for_all_users_in_context($context); $this->assertEquals($dbwrites, $DB->perf_get_writes()); $this->assertDebuggingNotCalled(); } /** * Create a test quiz for the specified course. * * @param \stdClass $course * @return array */ protected function create_test_quiz($course) { global $DB; $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance([ 'course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2, ]); // Create a couple of questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); quiz_add_quiz_question($saq->id, $quiz); $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); quiz_add_quiz_question($numq->id, $quiz); return $quiz; } /** * Answer questions for a quiz + user. * * @param \stdClass $quiz * @param \stdClass $user * @return array */ protected function attempt_quiz($quiz, $user) { $this->setUser($user); $starttime = time(); $quizobj = quiz::create($quiz->id, $user->id); $context = $quizobj->get_context(); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // Start the attempt. $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime); quiz_attempt_save_started($quizobj, $quba, $attempt); // Answer the questions. $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = [ 1 => ['answer' => 'frog'], 2 => ['answer' => '3.14'], ]; $attemptobj->process_submitted_actions($starttime, false, $tosubmit); // Finish the attempt. $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($starttime, false); $this->setUser(); return [$quizobj, $quba, $attemptobj]; } /** * Test for provider::get_users_in_context(). */ public function test_get_users_in_context() { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $anotheruser = $this->getDataGenerator()->create_user(); $extrauser = $this->getDataGenerator()->create_user(); // Make a quiz. $this->setUser(); $quiz = $this->create_test_quiz($course); // Create an override for user1. $DB->insert_record('quiz_overrides', [ 'quiz' => $quiz->id, 'userid' => $user->id, 'timeclose' => 1300, 'timelimit' => null, ]); // Make an attempt on the quiz as user2. list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $anotheruser); $context = $quizobj->get_context(); // Fetch users - user1 and user2 should be returned. $userlist = new \core_privacy\local\request\userlist($context, 'mod_quiz'); \mod_quiz\privacy\provider::get_users_in_context($userlist); $this->assertEquals( [$user->id, $anotheruser->id], $userlist->get_userids(), '', 0.0, 10, true); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users() { global $DB; $this->resetAfterTest(true); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Make a quiz in each course. $quiz1 = $this->create_test_quiz($course1); $quiz2 = $this->create_test_quiz($course2); // Attempt quiz1 as user1 and user2. list($quiz1obj) = $this->attempt_quiz($quiz1, $user1); $this->attempt_quiz($quiz1, $user2); // Create an override in quiz1 for user3. $DB->insert_record('quiz_overrides', [ 'quiz' => $quiz1->id, 'userid' => $user3->id, 'timeclose' => 1300, 'timelimit' => null, ]); // Attempt quiz2 as user1. $this->attempt_quiz($quiz2, $user1); // Delete the data for user1 and user3 in course1 and check it is removed. $quiz1context = $quiz1obj->get_context(); $approveduserlist = new \core_privacy\local\request\approved_userlist($quiz1context, 'mod_quiz', [$user1->id, $user3->id]); provider::delete_data_for_users($approveduserlist); // Only the attempt of user2 should be remained in quiz1. $this->assertEquals( [$user2->id], $DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz1->id]) ); // The attempt that user1 made in quiz2 should be remained. $this->assertEquals( [$user1->id], $DB->get_fieldset_select('quiz_attempts', 'userid', 'quiz = ?', [$quiz2->id]) ); // The quiz override in quiz1 that we had for user3 should be deleted. $this->assertEquals( [], $DB->get_fieldset_select('quiz_overrides', 'userid', 'quiz = ?', [$quiz1->id]) ); } }