MDL-63626 mod_quiz: Fixed a bug when there was no attempt on the quiz
[moodle.git] / mod / quiz / tests / privacy_provider_test.php
CommitLineData
4c729982
AN
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/>.
16
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 */
24
25use core_privacy\local\metadata\collection;
26use core_privacy\local\request\deletion_criteria;
27use core_privacy\local\request\writer;
28use mod_quiz\privacy\provider;
29use mod_quiz\privacy\helper;
30
31defined('MOODLE_INTERNAL') || die();
32
33global $CFG;
34require_once($CFG->dirroot . '/question/tests/privacy_helper.php');
35
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 */
43class mod_quiz_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
44
45 use core_question_privacy_helper;
46
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();
54
55 $contextlist = provider::get_contexts_for_userid($USER->id);
56 $this->assertEmpty($contextlist);
57 }
58
44b30f9c
SR
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);
65
66 $course = $this->getDataGenerator()->create_course();
67 $user = $this->getDataGenerator()->create_user();
68
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 ]);
78
79 $cm = get_coursemodule_from_instance('quiz', $quiz->id);
80 $context = \context_module::instance($cm->id);
81
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 }
88
4c729982
AN
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();
96
97 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
98 \core_user::get_user($USER->id),
99 'mod_quiz',
100 []
101 );
102
103 provider::export_user_data($approvedcontextlist);
104 $this->assertDebuggingNotCalled();
105
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 }
110
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();
118
119 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
120 \core_user::get_user($USER->id),
121 'mod_quiz',
122 []
123 );
124
125 provider::delete_data_for_user($approvedcontextlist);
126 $this->assertDebuggingNotCalled();
127 }
128
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);
135
4c729982
AN
136 $course = $this->getDataGenerator()->create_course();
137 $user = $this->getDataGenerator()->create_user();
63b0f627 138 $otheruser = $this->getDataGenerator()->create_user();
4c729982 139
63b0f627
AN
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,
4c729982
AN
148 ]);
149
4c729982 150 // Run as the user and make an attempt on the quiz.
63b0f627
AN
151 list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $user);
152 $this->attempt_quiz($quiz, $otheruser);
4c729982
AN
153 $context = $quizobj->get_context();
154
63b0f627 155 // Fetch the contexts - only one context should be returned.
4c729982
AN
156 $this->setUser();
157 $contextlist = provider::get_contexts_for_userid($user->id);
158 $this->assertCount(1, $contextlist);
159 $this->assertEquals($context, $contextlist->current());
160
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);
169
170 // Ensure that the quiz data was exported correctly.
171 $writer = writer::with_context($context);
172 $this->assertTrue($writer->has_any_data());
173
174 $quizdata = $writer->get_data([]);
175 $this->assertEquals($quizobj->get_quiz_name(), $quizdata->name);
176
177 // Every module has an intro.
178 $this->assertTrue(isset($quizdata->intro));
179
180 // Fetch the attempt data.
63b0f627 181 $attempt = $attemptobj->get_attempt();
4c729982
AN
182 $attemptsubcontext = [
183 get_string('attempts', 'mod_quiz'),
184 $attempt->attempt,
185 ];
186 $attemptdata = writer::with_context($context)->get_data($attemptsubcontext);
187
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));
196
197 $this->assertTrue(isset($attemptdata->grade));
198 $this->assertEquals(100.00, $attemptdata->grade->grade);
199
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 );
209
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 }
216
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);
223
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');
228
229 $quiz = $quizgenerator->create_instance([
230 'course' => $course->id,
231 'questionsperpage' => 0,
232 'grade' => 100.0,
233 'sumgrades' => 2,
234 ]);
235
236 // Create a couple of questions.
237 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
238 $cat = $questiongenerator->create_question_category();
239
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);
244
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();
250
251 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
252 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
253
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);
258
259 // Answer the questions.
260 $attemptobj = quiz_attempt::create($attempt->id);
261
262 $tosubmit = [
263 1 => ['answer' => 'frog'],
264 2 => ['answer' => '3.14'],
265 ];
266
267 $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
268
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);
273
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 }
63b0f627
AN
279
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);
286
287 $course = $this->getDataGenerator()->create_course();
288 $user = $this->getDataGenerator()->create_user();
289 $otheruser = $this->getDataGenerator()->create_user();
290
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 ]);
300
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);
304
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 ]);
314
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);
318
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);
323
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()]));
328
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 }
334
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);
341
342 $course = $this->getDataGenerator()->create_course();
343 $user = $this->getDataGenerator()->create_user();
344
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);
351
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);
356
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);
365
366 // Ensure that nothing was exported.
367 $writer = writer::with_context($context);
368 $this->assertFalse($writer->has_any_data_in_any_context());
369
370 $this->setUser();
371
372 $dbwrites = $DB->perf_get_writes();
373
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();
383
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 }
389
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;
398
399 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
400
401 $quiz = $quizgenerator->create_instance([
402 'course' => $course->id,
403 'questionsperpage' => 0,
404 'grade' => 100.0,
405 'sumgrades' => 2,
406 ]);
407
408 // Create a couple of questions.
409 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
410 $cat = $questiongenerator->create_question_category();
411
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);
416
417 return $quiz;
418 }
419
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);
429
430 $starttime = time();
431 $quizobj = quiz::create($quiz->id, $user->id);
432 $context = $quizobj->get_context();
433
434 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
435 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
436
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);
441
442 // Answer the questions.
443 $attemptobj = quiz_attempt::create($attempt->id);
444
445 $tosubmit = [
446 1 => ['answer' => 'frog'],
447 2 => ['answer' => '3.14'],
448 ];
449
450 $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
451
452 // Finish the attempt.
453 $attemptobj = quiz_attempt::create($attempt->id);
454 $attemptobj->process_finish($starttime, false);
455
456 $this->setUser();
457
458 return [$quizobj, $quba, $attemptobj];
459 }
4c729982 460}