MDL-63626 mod_quiz: Fixed a bug when there was no attempt on the quiz
[moodle.git] / mod / quiz / tests / privacy_provider_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Privacy provider tests.
19  *
20  * @package    mod_quiz
21  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 use core_privacy\local\metadata\collection;
26 use core_privacy\local\request\deletion_criteria;
27 use core_privacy\local\request\writer;
28 use mod_quiz\privacy\provider;
29 use mod_quiz\privacy\helper;
31 defined('MOODLE_INTERNAL') || die();
33 global $CFG;
34 require_once($CFG->dirroot . '/question/tests/privacy_helper.php');
36 /**
37  * Privacy provider tests class.
38  *
39  * @package    mod_quiz
40  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
41  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42  */
43 class mod_quiz_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
45     use core_question_privacy_helper;
47     /**
48      * Test that a user who has no data gets no contexts
49      */
50     public function test_get_contexts_for_userid_no_data() {
51         global $USER;
52         $this->resetAfterTest();
53         $this->setAdminUser();
55         $contextlist = provider::get_contexts_for_userid($USER->id);
56         $this->assertEmpty($contextlist);
57     }
59     /**
60      * Test for provider::get_contexts_for_userid() when there is no quiz attempt at all.
61      */
62     public function test_get_contexts_for_userid_no_attempt_with_override() {
63         global $DB;
64         $this->resetAfterTest(true);
66         $course = $this->getDataGenerator()->create_course();
67         $user = $this->getDataGenerator()->create_user();
69         // Make a quiz with an override.
70         $this->setUser();
71         $quiz = $this->create_test_quiz($course);
72         $DB->insert_record('quiz_overrides', [
73             'quiz' => $quiz->id,
74             'userid' => $user->id,
75             'timeclose' => 1300,
76             'timelimit' => null,
77         ]);
79         $cm = get_coursemodule_from_instance('quiz', $quiz->id);
80         $context = \context_module::instance($cm->id);
82         // Fetch the contexts - only one context should be returned.
83         $this->setUser();
84         $contextlist = provider::get_contexts_for_userid($user->id);
85         $this->assertCount(1, $contextlist);
86         $this->assertEquals($context, $contextlist->current());
87     }
89     /**
90      * The export function should handle an empty contextlist properly.
91      */
92     public function test_export_user_data_no_data() {
93         global $USER;
94         $this->resetAfterTest();
95         $this->setAdminUser();
97         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
98             \core_user::get_user($USER->id),
99             'mod_quiz',
100             []
101         );
103         provider::export_user_data($approvedcontextlist);
104         $this->assertDebuggingNotCalled();
106         // No data should have been exported.
107         $writer = \core_privacy\local\request\writer::with_context(\context_system::instance());
108         $this->assertFalse($writer->has_any_data_in_any_context());
109     }
111     /**
112      * The delete function should handle an empty contextlist properly.
113      */
114     public function test_delete_data_for_user_no_data() {
115         global $USER;
116         $this->resetAfterTest();
117         $this->setAdminUser();
119         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
120             \core_user::get_user($USER->id),
121             'mod_quiz',
122             []
123         );
125         provider::delete_data_for_user($approvedcontextlist);
126         $this->assertDebuggingNotCalled();
127     }
129     /**
130      * Export + Delete quiz data for a user who has made a single attempt.
131      */
132     public function test_user_with_data() {
133         global $DB;
134         $this->resetAfterTest(true);
136         $course = $this->getDataGenerator()->create_course();
137         $user = $this->getDataGenerator()->create_user();
138         $otheruser = $this->getDataGenerator()->create_user();
140         // Make a quiz with an override.
141         $this->setUser();
142         $quiz = $this->create_test_quiz($course);
143         $DB->insert_record('quiz_overrides', [
144                 'quiz' => $quiz->id,
145                 'userid' => $user->id,
146                 'timeclose' => 1300,
147                 'timelimit' => null,
148             ]);
150         // Run as the user and make an attempt on the quiz.
151         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
152         $this->attempt_quiz($quiz, $otheruser);
153         $context = $quizobj->get_context();
155         // Fetch the contexts - only one context should be returned.
156         $this->setUser();
157         $contextlist = provider::get_contexts_for_userid($user->id);
158         $this->assertCount(1, $contextlist);
159         $this->assertEquals($context, $contextlist->current());
161         // Perform the export and check the data.
162         $this->setUser($user);
163         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
164             \core_user::get_user($user->id),
165             'mod_quiz',
166             $contextlist->get_contextids()
167         );
168         provider::export_user_data($approvedcontextlist);
170         // Ensure that the quiz data was exported correctly.
171         $writer = writer::with_context($context);
172         $this->assertTrue($writer->has_any_data());
174         $quizdata = $writer->get_data([]);
175         $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name);
177         // Every module has an intro.
178         $this->assertTrue(isset($quizdata->intro));
180         // Fetch the attempt data.
181         $attempt = $attemptobj->get_attempt();
182         $attemptsubcontext = [
183             get_string('attempts', 'mod_quiz'),
184             $attempt->attempt,
185         ];
186         $attemptdata = writer::with_context($context)->get_data($attemptsubcontext);
188         $attempt = $attemptobj->get_attempt();
189         $this->assertTrue(isset($attemptdata->state));
190         $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state);
191         $this->assertTrue(isset($attemptdata->timestart));
192         $this->assertTrue(isset($attemptdata->timefinish));
193         $this->assertTrue(isset($attemptdata->timemodified));
194         $this->assertFalse(isset($attemptdata->timemodifiedoffline));
195         $this->assertFalse(isset($attemptdata->timecheckstate));
197         $this->assertTrue(isset($attemptdata->grade));
198         $this->assertEquals(100.00, $attemptdata->grade->grade);
200         // Check that the exported question attempts are correct.
201         $attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user);
202         $this->assert_question_attempt_exported(
203             $context,
204             $attemptsubcontext,
205             \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()),
206             quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context),
207             $user
208         );
210         // Delete the data and check it is removed.
211         $this->setUser();
212         provider::delete_data_for_user($approvedcontextlist);
213         $this->expectException(\dml_missing_record_exception::class);
214         \quiz_attempt::create($attemptobj->get_quizid());
215     }
217     /**
218      * Export + Delete quiz data for a user who has made a single attempt.
219      */
220     public function test_user_with_preview() {
221         global $DB;
222         $this->resetAfterTest(true);
224         // Make a quiz.
225         $course = $this->getDataGenerator()->create_course();
226         $user = $this->getDataGenerator()->create_user();
227         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
229         $quiz = $quizgenerator->create_instance([
230                 'course' => $course->id,
231                 'questionsperpage' => 0,
232                 'grade' => 100.0,
233                 'sumgrades' => 2,
234             ]);
236         // Create a couple of questions.
237         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
238         $cat = $questiongenerator->create_question_category();
240         $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
241         quiz_add_quiz_question($saq->id, $quiz);
242         $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
243         quiz_add_quiz_question($numq->id, $quiz);
245         // Run as the user and make an attempt on the quiz.
246         $this->setUser($user);
247         $starttime = time();
248         $quizobj = quiz::create($quiz->id, $user->id);
249         $context = $quizobj->get_context();
251         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
252         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
254         // Start the attempt.
255         $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id);
256         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
257         quiz_attempt_save_started($quizobj, $quba, $attempt);
259         // Answer the questions.
260         $attemptobj = quiz_attempt::create($attempt->id);
262         $tosubmit = [
263             1 => ['answer' => 'frog'],
264             2 => ['answer' => '3.14'],
265         ];
267         $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
269         // Finish the attempt.
270         $attemptobj = quiz_attempt::create($attempt->id);
271         $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
272         $attemptobj->process_finish($starttime, false);
274         // Fetch the contexts - no context should be returned.
275         $this->setUser();
276         $contextlist = provider::get_contexts_for_userid($user->id);
277         $this->assertCount(0, $contextlist);
278     }
280     /**
281      * Export + Delete quiz data for a user who has made a single attempt.
282      */
283     public function test_delete_data_for_all_users_in_context() {
284         global $DB;
285         $this->resetAfterTest(true);
287         $course = $this->getDataGenerator()->create_course();
288         $user = $this->getDataGenerator()->create_user();
289         $otheruser = $this->getDataGenerator()->create_user();
291         // Make a quiz with an override.
292         $this->setUser();
293         $quiz = $this->create_test_quiz($course);
294         $DB->insert_record('quiz_overrides', [
295                 'quiz' => $quiz->id,
296                 'userid' => $user->id,
297                 'timeclose' => 1300,
298                 'timelimit' => null,
299             ]);
301         // Run as the user and make an attempt on the quiz.
302         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
303         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $otheruser);
305         // Create another quiz and questions, and repeat the data insertion.
306         $this->setUser();
307         $otherquiz = $this->create_test_quiz($course);
308         $DB->insert_record('quiz_overrides', [
309                 'quiz' => $otherquiz->id,
310                 'userid' => $user->id,
311                 'timeclose' => 1300,
312                 'timelimit' => null,
313             ]);
315         // Run as the user and make an attempt on the quiz.
316         list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $user);
317         list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $otheruser);
319         // Delete all data for all users in the context under test.
320         $this->setUser();
321         $context = $quizobj->get_context();
322         provider::delete_data_for_all_users_in_context($context);
324         // The quiz attempt should have been deleted from this quiz.
325         $this->assertCount(0, $DB->get_records('quiz_attempts', ['quiz' => $quizobj->get_quizid()]));
326         $this->assertCount(0, $DB->get_records('quiz_overrides', ['quiz' => $quizobj->get_quizid()]));
327         $this->assertCount(0, $DB->get_records('question_attempts', ['questionusageid' => $quba->get_id()]));
329         // But not for the other quiz.
330         $this->assertNotCount(0, $DB->get_records('quiz_attempts', ['quiz' => $otherquizobj->get_quizid()]));
331         $this->assertNotCount(0, $DB->get_records('quiz_overrides', ['quiz' => $otherquizobj->get_quizid()]));
332         $this->assertNotCount(0, $DB->get_records('question_attempts', ['questionusageid' => $otherquba->get_id()]));
333     }
335     /**
336      * Export + Delete quiz data for a user who has made a single attempt.
337      */
338     public function test_wrong_context() {
339         global $DB;
340         $this->resetAfterTest(true);
342         $course = $this->getDataGenerator()->create_course();
343         $user = $this->getDataGenerator()->create_user();
345         // Make a choice.
346         $this->setUser();
347         $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
348         $choice = $plugingenerator->create_instance(['course' => $course->id]);
349         $cm = get_coursemodule_from_instance('choice', $choice->id);
350         $context = \context_module::instance($cm->id);
352         // Fetch the contexts - no context should be returned.
353         $this->setUser();
354         $contextlist = provider::get_contexts_for_userid($user->id);
355         $this->assertCount(0, $contextlist);
357         // Perform the export and check the data.
358         $this->setUser($user);
359         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
360             \core_user::get_user($user->id),
361             'mod_quiz',
362             [$context->id]
363         );
364         provider::export_user_data($approvedcontextlist);
366         // Ensure that nothing was exported.
367         $writer = writer::with_context($context);
368         $this->assertFalse($writer->has_any_data_in_any_context());
370         $this->setUser();
372         $dbwrites = $DB->perf_get_writes();
374         // Perform a deletion with the approved contextlist containing an incorrect context.
375         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
376             \core_user::get_user($user->id),
377             'mod_quiz',
378             [$context->id]
379         );
380         provider::delete_data_for_user($approvedcontextlist);
381         $this->assertEquals($dbwrites, $DB->perf_get_writes());
382         $this->assertDebuggingNotCalled();
384         // Perform a deletion of all data in the context.
385         provider::delete_data_for_all_users_in_context($context);
386         $this->assertEquals($dbwrites, $DB->perf_get_writes());
387         $this->assertDebuggingNotCalled();
388     }
390     /**
391      * Create a test quiz for the specified course.
392      *
393      * @param   \stdClass $course
394      * @return  array
395      */
396     protected function create_test_quiz($course) {
397         global $DB;
399         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
401         $quiz = $quizgenerator->create_instance([
402                 'course' => $course->id,
403                 'questionsperpage' => 0,
404                 'grade' => 100.0,
405                 'sumgrades' => 2,
406             ]);
408         // Create a couple of questions.
409         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
410         $cat = $questiongenerator->create_question_category();
412         $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
413         quiz_add_quiz_question($saq->id, $quiz);
414         $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
415         quiz_add_quiz_question($numq->id, $quiz);
417         return $quiz;
418     }
420     /**
421      * Answer questions for a quiz + user.
422      *
423      * @param   \stdClass   $quiz
424      * @param   \stdClass   $user
425      * @return  array
426      */
427     protected function attempt_quiz($quiz, $user) {
428         $this->setUser($user);
430         $starttime = time();
431         $quizobj = quiz::create($quiz->id, $user->id);
432         $context = $quizobj->get_context();
434         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
435         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
437         // Start the attempt.
438         $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id);
439         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
440         quiz_attempt_save_started($quizobj, $quba, $attempt);
442         // Answer the questions.
443         $attemptobj = quiz_attempt::create($attempt->id);
445         $tosubmit = [
446             1 => ['answer' => 'frog'],
447             2 => ['answer' => '3.14'],
448         ];
450         $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
452         // Finish the attempt.
453         $attemptobj = quiz_attempt::create($attempt->id);
454         $attemptobj->process_finish($starttime, false);
456         $this->setUser();
458         return [$quizobj, $quba, $attemptobj];
459     }