9e5ca4b7f3262ecf1566d01dd9bcbde316cc8955
[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      * The export function should handle an empty contextlist properly.
61      */
62     public function test_export_user_data_no_data() {
63         global $USER;
64         $this->resetAfterTest();
65         $this->setAdminUser();
67         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
68             \core_user::get_user($USER->id),
69             'mod_quiz',
70             []
71         );
73         provider::export_user_data($approvedcontextlist);
74         $this->assertDebuggingNotCalled();
76         // No data should have been exported.
77         $writer = \core_privacy\local\request\writer::with_context(\context_system::instance());
78         $this->assertFalse($writer->has_any_data_in_any_context());
79     }
81     /**
82      * The delete function should handle an empty contextlist properly.
83      */
84     public function test_delete_data_for_user_no_data() {
85         global $USER;
86         $this->resetAfterTest();
87         $this->setAdminUser();
89         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
90             \core_user::get_user($USER->id),
91             'mod_quiz',
92             []
93         );
95         provider::delete_data_for_user($approvedcontextlist);
96         $this->assertDebuggingNotCalled();
97     }
99     /**
100      * Export + Delete quiz data for a user who has made a single attempt.
101      */
102     public function test_user_with_data() {
103         global $DB;
104         $this->resetAfterTest(true);
106         $course = $this->getDataGenerator()->create_course();
107         $user = $this->getDataGenerator()->create_user();
108         $otheruser = $this->getDataGenerator()->create_user();
110         // Make a quiz with an override.
111         $this->setUser();
112         $quiz = $this->create_test_quiz($course);
113         $DB->insert_record('quiz_overrides', [
114                 'quiz' => $quiz->id,
115                 'userid' => $user->id,
116                 'timeclose' => 1300,
117                 'timelimit' => null,
118             ]);
120         // Run as the user and make an attempt on the quiz.
121         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
122         $this->attempt_quiz($quiz, $otheruser);
123         $context = $quizobj->get_context();
125         // Fetch the contexts - only one context should be returned.
126         $this->setUser();
127         $contextlist = provider::get_contexts_for_userid($user->id);
128         $this->assertCount(1, $contextlist);
129         $this->assertEquals($context, $contextlist->current());
131         // Perform the export and check the data.
132         $this->setUser($user);
133         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
134             \core_user::get_user($user->id),
135             'mod_quiz',
136             $contextlist->get_contextids()
137         );
138         provider::export_user_data($approvedcontextlist);
140         // Ensure that the quiz data was exported correctly.
141         $writer = writer::with_context($context);
142         $this->assertTrue($writer->has_any_data());
144         $quizdata = $writer->get_data([]);
145         $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name);
147         // Every module has an intro.
148         $this->assertTrue(isset($quizdata->intro));
150         // Fetch the attempt data.
151         $attempt = $attemptobj->get_attempt();
152         $attemptsubcontext = [
153             get_string('attempts', 'mod_quiz'),
154             $attempt->attempt,
155         ];
156         $attemptdata = writer::with_context($context)->get_data($attemptsubcontext);
158         $attempt = $attemptobj->get_attempt();
159         $this->assertTrue(isset($attemptdata->state));
160         $this->assertEquals(\quiz_attempt::state_name($attemptobj->get_state()), $attemptdata->state);
161         $this->assertTrue(isset($attemptdata->timestart));
162         $this->assertTrue(isset($attemptdata->timefinish));
163         $this->assertTrue(isset($attemptdata->timemodified));
164         $this->assertFalse(isset($attemptdata->timemodifiedoffline));
165         $this->assertFalse(isset($attemptdata->timecheckstate));
167         $this->assertTrue(isset($attemptdata->grade));
168         $this->assertEquals(100.00, $attemptdata->grade->grade);
170         // Check that the exported question attempts are correct.
171         $attemptsubcontext = helper::get_quiz_attempt_subcontext($attemptobj->get_attempt(), $user);
172         $this->assert_question_attempt_exported(
173             $context,
174             $attemptsubcontext,
175             \question_engine::load_questions_usage_by_activity($attemptobj->get_uniqueid()),
176             quiz_get_review_options($quiz, $attemptobj->get_attempt(), $context),
177             $user
178         );
180         // Delete the data and check it is removed.
181         $this->setUser();
182         provider::delete_data_for_user($approvedcontextlist);
183         $this->expectException(\dml_missing_record_exception::class);
184         \quiz_attempt::create($attemptobj->get_quizid());
185     }
187     /**
188      * Export + Delete quiz data for a user who has made a single attempt.
189      */
190     public function test_user_with_preview() {
191         global $DB;
192         $this->resetAfterTest(true);
194         // Make a quiz.
195         $course = $this->getDataGenerator()->create_course();
196         $user = $this->getDataGenerator()->create_user();
197         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
199         $quiz = $quizgenerator->create_instance([
200                 'course' => $course->id,
201                 'questionsperpage' => 0,
202                 'grade' => 100.0,
203                 'sumgrades' => 2,
204             ]);
206         // Create a couple of questions.
207         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
208         $cat = $questiongenerator->create_question_category();
210         $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
211         quiz_add_quiz_question($saq->id, $quiz);
212         $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
213         quiz_add_quiz_question($numq->id, $quiz);
215         // Run as the user and make an attempt on the quiz.
216         $this->setUser($user);
217         $starttime = time();
218         $quizobj = quiz::create($quiz->id, $user->id);
219         $context = $quizobj->get_context();
221         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
222         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
224         // Start the attempt.
225         $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, true, $user->id);
226         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
227         quiz_attempt_save_started($quizobj, $quba, $attempt);
229         // Answer the questions.
230         $attemptobj = quiz_attempt::create($attempt->id);
232         $tosubmit = [
233             1 => ['answer' => 'frog'],
234             2 => ['answer' => '3.14'],
235         ];
237         $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
239         // Finish the attempt.
240         $attemptobj = quiz_attempt::create($attempt->id);
241         $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
242         $attemptobj->process_finish($starttime, false);
244         // Fetch the contexts - no context should be returned.
245         $this->setUser();
246         $contextlist = provider::get_contexts_for_userid($user->id);
247         $this->assertCount(0, $contextlist);
248     }
250     /**
251      * Export + Delete quiz data for a user who has made a single attempt.
252      */
253     public function test_delete_data_for_all_users_in_context() {
254         global $DB;
255         $this->resetAfterTest(true);
257         $course = $this->getDataGenerator()->create_course();
258         $user = $this->getDataGenerator()->create_user();
259         $otheruser = $this->getDataGenerator()->create_user();
261         // Make a quiz with an override.
262         $this->setUser();
263         $quiz = $this->create_test_quiz($course);
264         $DB->insert_record('quiz_overrides', [
265                 'quiz' => $quiz->id,
266                 'userid' => $user->id,
267                 'timeclose' => 1300,
268                 'timelimit' => null,
269             ]);
271         // Run as the user and make an attempt on the quiz.
272         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
273         list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $otheruser);
275         // Create another quiz and questions, and repeat the data insertion.
276         $this->setUser();
277         $otherquiz = $this->create_test_quiz($course);
278         $DB->insert_record('quiz_overrides', [
279                 'quiz' => $otherquiz->id,
280                 'userid' => $user->id,
281                 'timeclose' => 1300,
282                 'timelimit' => null,
283             ]);
285         // Run as the user and make an attempt on the quiz.
286         list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $user);
287         list($otherquizobj, $otherquba, $otherattemptobj) = $this->attempt_quiz($otherquiz, $otheruser);
289         // Delete all data for all users in the context under test.
290         $this->setUser();
291         $context = $quizobj->get_context();
292         provider::delete_data_for_all_users_in_context($context);
294         // The quiz attempt should have been deleted from this quiz.
295         $this->assertCount(0, $DB->get_records('quiz_attempts', ['quiz' => $quizobj->get_quizid()]));
296         $this->assertCount(0, $DB->get_records('quiz_overrides', ['quiz' => $quizobj->get_quizid()]));
297         $this->assertCount(0, $DB->get_records('question_attempts', ['questionusageid' => $quba->get_id()]));
299         // But not for the other quiz.
300         $this->assertNotCount(0, $DB->get_records('quiz_attempts', ['quiz' => $otherquizobj->get_quizid()]));
301         $this->assertNotCount(0, $DB->get_records('quiz_overrides', ['quiz' => $otherquizobj->get_quizid()]));
302         $this->assertNotCount(0, $DB->get_records('question_attempts', ['questionusageid' => $otherquba->get_id()]));
303     }
305     /**
306      * Export + Delete quiz data for a user who has made a single attempt.
307      */
308     public function test_wrong_context() {
309         global $DB;
310         $this->resetAfterTest(true);
312         $course = $this->getDataGenerator()->create_course();
313         $user = $this->getDataGenerator()->create_user();
315         // Make a choice.
316         $this->setUser();
317         $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
318         $choice = $plugingenerator->create_instance(['course' => $course->id]);
319         $cm = get_coursemodule_from_instance('choice', $choice->id);
320         $context = \context_module::instance($cm->id);
322         // Fetch the contexts - no context should be returned.
323         $this->setUser();
324         $contextlist = provider::get_contexts_for_userid($user->id);
325         $this->assertCount(0, $contextlist);
327         // Perform the export and check the data.
328         $this->setUser($user);
329         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
330             \core_user::get_user($user->id),
331             'mod_quiz',
332             [$context->id]
333         );
334         provider::export_user_data($approvedcontextlist);
336         // Ensure that nothing was exported.
337         $writer = writer::with_context($context);
338         $this->assertFalse($writer->has_any_data_in_any_context());
340         $this->setUser();
342         $dbwrites = $DB->perf_get_writes();
344         // Perform a deletion with the approved contextlist containing an incorrect context.
345         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
346             \core_user::get_user($user->id),
347             'mod_quiz',
348             [$context->id]
349         );
350         provider::delete_data_for_user($approvedcontextlist);
351         $this->assertEquals($dbwrites, $DB->perf_get_writes());
352         $this->assertDebuggingNotCalled();
354         // Perform a deletion of all data in the context.
355         provider::delete_data_for_all_users_in_context($context);
356         $this->assertEquals($dbwrites, $DB->perf_get_writes());
357         $this->assertDebuggingNotCalled();
358     }
360     /**
361      * Create a test quiz for the specified course.
362      *
363      * @param   \stdClass $course
364      * @return  array
365      */
366     protected function create_test_quiz($course) {
367         global $DB;
369         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
371         $quiz = $quizgenerator->create_instance([
372                 'course' => $course->id,
373                 'questionsperpage' => 0,
374                 'grade' => 100.0,
375                 'sumgrades' => 2,
376             ]);
378         // Create a couple of questions.
379         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
380         $cat = $questiongenerator->create_question_category();
382         $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
383         quiz_add_quiz_question($saq->id, $quiz);
384         $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
385         quiz_add_quiz_question($numq->id, $quiz);
387         return $quiz;
388     }
390     /**
391      * Answer questions for a quiz + user.
392      *
393      * @param   \stdClass   $quiz
394      * @param   \stdClass   $user
395      * @return  array
396      */
397     protected function attempt_quiz($quiz, $user) {
398         $this->setUser($user);
400         $starttime = time();
401         $quizobj = quiz::create($quiz->id, $user->id);
402         $context = $quizobj->get_context();
404         $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
405         $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
407         // Start the attempt.
408         $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id);
409         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
410         quiz_attempt_save_started($quizobj, $quba, $attempt);
412         // Answer the questions.
413         $attemptobj = quiz_attempt::create($attempt->id);
415         $tosubmit = [
416             1 => ['answer' => 'frog'],
417             2 => ['answer' => '3.14'],
418         ];
420         $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
422         // Finish the attempt.
423         $attemptobj = quiz_attempt::create($attempt->id);
424         $attemptobj->process_finish($starttime, false);
426         $this->setUser();
428         return [$quizobj, $quba, $attemptobj];
429     }