Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace mod_quiz\local;
18
 
19
use mod_quiz\event\group_override_created;
20
use mod_quiz\event\group_override_updated;
21
use mod_quiz\event\user_override_created;
22
use mod_quiz\event\user_override_updated;
23
use mod_quiz\event\user_override_deleted;
24
use mod_quiz\quiz_settings;
25
 
26
/**
27
 * Test for override_manager class
28
 *
29
 * @package   mod_quiz
30
 * @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
31
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 * @covers    \mod_quiz\local\override_manager
33
 */
34
final class override_manager_test extends \advanced_testcase {
35
    /** @var array Default quiz settings **/
36
    private const TEST_QUIZ_SETTINGS = [
37
        'attempts' => 5,
38
        'timeopen' => 100000000,
39
        'timeclose' => 10000001,
40
        'timelimit' => 10,
41
    ];
42
 
43
    /**
44
     * Create quiz and course for test
45
     *
46
     * @return array containing quiz object and course
47
     */
48
    private function create_quiz_and_course(): array {
11 efrain 49
        $course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]);
1 efrain 50
        $quizparams = array_merge(self::TEST_QUIZ_SETTINGS, ['course' => $course->id]);
51
        $quiz = $this->getDataGenerator()->create_module('quiz', $quizparams);
52
        $quizobj = quiz_settings::create($quiz->id);
53
        return [$quizobj, $course];
54
    }
55
 
56
    /**
57
     * Utility function that replaces the placeholders in the given data.
58
     *
59
     * @param array $data
60
     * @param array $placeholdervalues
61
     * @return array the $data with the placeholders replaced
62
     */
63
    private function replace_placeholders(array $data, array $placeholdervalues) {
64
        foreach ($data as $key => $value) {
65
            $replacement = $placeholdervalues[$value] ?? null;
66
 
67
            if (!empty($replacement)) {
68
                $data[$key] = $replacement;
69
            }
70
        }
71
 
72
        return $data;
73
    }
74
 
75
    /**
76
     * Utility function that sets up data for tests testing CRUD operations.
77
     * Placeholders such as ':userid' and ':groupid' can be used in the data to replace with the relevant id.
78
     *
79
     * @param array $existingdata Data used to setup a preexisting quiz override record.
80
     * @param array $formdata submitted formdata
81
     * @return \stdClass containing formdata (after placeholders replaced), quizobj, user and group
82
     */
83
    private function setup_existing_and_testing_data(array $existingdata, array $formdata): \stdClass {
84
        global $DB;
85
 
86
        [$quizobj, $course] = $this->create_quiz_and_course();
87
 
88
        $user = $this->getDataGenerator()->create_user();
89
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
90
 
91
        $user2 = $this->getDataGenerator()->create_user();
92
        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
93
 
94
        $groupid = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test']);
95
        $group2id = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test2']);
96
 
97
        // Replace any userid or groupid placeholders in the form data or existing data.
98
        $placeholdervalues = [
99
            ':userid' => $user->id,
100
            ':user2id' => $user2->id,
101
            ':groupid' => $groupid,
102
            ':group2id' => $group2id,
103
        ];
104
 
105
        if (!empty($existingdata)) {
106
            // Raw insert the existing data for the test into the DB.
107
            // We assume it is valid for the test.
108
            $existingdata['quiz'] = $quizobj->get_quizid();
109
            $existingid = $DB->insert_record('quiz_overrides', $this->replace_placeholders($existingdata, $placeholdervalues));
110
            $placeholdervalues[':existingid'] = $existingid;
111
        }
112
 
113
        $formdata = $this->replace_placeholders($formdata, $placeholdervalues);
114
 
115
        // Add quiz id to formdata.
116
        $formdata['quiz'] = $quizobj->get_quizid();
117
 
118
        return (object) [
119
            'quizobj' => $quizobj,
120
            'formdata' => $formdata,
121
            'user1' => $user,
122
            'groupid1' => $groupid,
123
        ];
124
    }
125
 
126
    /**
11 efrain 127
     * Data provider for {@see test_can_view_override}
128
     *
129
     * @return array[]
130
     */
131
    public static function can_view_override_provider(): array {
132
        return [
133
            ['admin', true, true, true, true],
134
            ['teacher', true, false, true, false],
135
        ];
136
    }
137
 
138
    /**
139
     * Test whether user can view given override
140
     *
141
     * @param string $currentuser
142
     * @param bool $grouponeview
143
     * @param bool $grouptwoview
144
     * @param bool $studentoneview
145
     * @param bool $studenttwoview
146
     *
147
     * @dataProvider can_view_override_provider
148
     */
149
    public function test_can_view_override(
150
        string $currentuser,
151
        bool $grouponeview,
152
        bool $grouptwoview,
153
        bool $studentoneview,
154
        bool $studenttwoview,
155
    ): void {
156
        global $DB;
157
 
158
        $this->resetAfterTest();
159
 
160
        [$quizobj, $course] = $this->create_quiz_and_course();
161
 
162
        // Teacher cannot view all groups.
163
        $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']);
164
        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $quizobj->get_context()->id);
165
 
166
        // Group one will contain our teacher and another student.
167
        $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
168
        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher', ['username' => 'teacher']);
169
        $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]);
170
        $studentone = $this->getDataGenerator()->create_and_enrol($course);
171
        $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $studentone->id]);
172
 
173
        // Group two will contain a solitary student.
174
        $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
175
        $studenttwo = $this->getDataGenerator()->create_and_enrol($course);
176
        $this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $studenttwo->id]);
177
 
178
        $user = \core_user::get_user_by_username($currentuser);
179
        $this->setUser($user);
180
 
181
        /** @var override_manager $manager */
182
        $manager = $quizobj->get_override_manager();
183
 
184
        $this->assertEquals($grouponeview, $manager->can_view_override(
185
            (object) ['groupid' => $groupone->id, 'userid' => null],
186
            $course,
187
            $quizobj->get_cm(),
188
        ));
189
 
190
        $this->assertEquals($grouptwoview, $manager->can_view_override(
191
            (object) ['groupid' => $grouptwo->id, 'userid' => null],
192
            $course,
193
            $quizobj->get_cm(),
194
        ));
195
 
196
        $this->assertEquals($studentoneview, $manager->can_view_override(
197
            (object) ['userid' => $studentone->id, 'groupid' => null],
198
            $course,
199
            $quizobj->get_cm(),
200
        ));
201
 
202
        $this->assertEquals($studenttwoview, $manager->can_view_override(
203
            (object) ['userid' => $studenttwo->id, 'groupid' => null],
204
            $course,
205
            $quizobj->get_cm(),
206
        ));
207
    }
208
 
209
    /**
1 efrain 210
     * Provides values to test_save_and_get_override
211
     *
212
     * @return array
213
     */
214
    public static function save_and_get_override_provider(): array {
215
        return [
216
            'create user override - no existing data' => [
217
                'existingdata' => [],
218
                'formdata' => [
219
                    'userid' => ':userid',
220
                    'groupid' => null,
221
                    'timeopen' => 50,
222
                    'timeclose' => 51,
223
                    'timelimit' => 1,
224
                    'attempts' => 999,
225
                    'password' => 'test',
226
                ],
227
                'expectedrecordscreated' => 1,
228
                'expectedevent' => user_override_created::class,
229
            ],
230
            'create user override - no calendar events should be created' => [
231
                'existingdata' => [],
232
                'formdata' => [
233
                    'userid' => ':userid',
234
                    'groupid' => null,
235
                    'timeopen' => null,
236
                    'timeclose' => null,
237
                    'timelimit' => 1,
238
                    'attempts' => 999,
239
                    'password' => 'test',
240
                ],
241
                'expectedrecordscreated' => 1,
242
                'expectedevent' => user_override_created::class,
243
            ],
244
            'create user override - only timeopen' => [
245
                'existingdata' => [],
246
                'formdata' => [
247
                    'userid' => ':userid',
248
                    'groupid' => null,
249
                    'timeopen' => 50,
250
                    'timeclose' => null,
251
                    'timelimit' => null,
252
                    'attempts' => null,
253
                    'password' => 'test',
254
                ],
255
                'expectedrecordscreated' => 1,
256
                'expectedevent' => user_override_created::class,
257
            ],
258
            'create group override - no existing data' => [
259
                'existingdata' => [],
260
                'formdata' => [
261
                    'userid' => null,
262
                    'groupid' => ':groupid',
263
                    'timeopen' => 50,
264
                    'timeclose' => 51,
265
                    'timelimit' => 1,
266
                    'attempts' => 999,
267
                    'password' => 'test',
268
                ],
269
                'expectedrecordscreated' => 1,
270
                'expectedevent' => group_override_created::class,
271
            ],
272
            'create group override - no calendar events should be created' => [
273
                'existingdata' => [],
274
                'formdata' => [
275
                    'userid' => null,
276
                    'groupid' => ':groupid',
277
                    'timeopen' => null,
278
                    'timeclose' => null,
279
                    'timelimit' => 1,
280
                    'attempts' => 999,
281
                    'password' => 'test',
282
                ],
283
                'expectedrecordscreated' => 1,
284
                'expectedevent' => group_override_created::class,
285
            ],
286
            'create group override - only timeopen' => [
287
                'existingdata' => [],
288
                'formdata' => [
289
                    'userid' => null,
290
                    'groupid' => ':groupid',
291
                    'timeopen' => 50,
292
                    'timeclose' => null,
293
                    'timelimit' => null,
294
                    'attempts' => null,
295
                    'password' => null,
296
                ],
297
                'expectedrecordscreated' => 1,
298
                'expectedevent' => group_override_created::class,
299
            ],
300
            'update user override - updating existing data' => [
301
                'existingdata' => [
302
                    'userid' => ':userid',
303
                    'groupid' => null,
304
                    'timeopen' => 50,
305
                    'timeclose' => 51,
306
                    'timelimit' => 2,
307
                    'attempts' => 2,
308
                    'password' => 'test2',
309
                ],
310
                'formdata' => [
311
                    'id' => ':existingid',
312
                    'userid' => ':userid',
313
                    'groupid' => null,
314
                    'timeopen' => 52,
315
                    'timeclose' => 53,
316
                    'timelimit' => 1,
317
                    'attempts' => 999,
318
                    'password' => 'test',
319
                ],
320
                'expectedrecordscreated' => 0,
321
                'expectedevent' => user_override_updated::class,
322
            ],
323
            'update group override - updating existing data' => [
324
                'existingdata' => [
325
                    'userid' => null,
326
                    'groupid' => ':groupid',
327
                    'timeopen' => 50,
328
                    'timeclose' => 51,
329
                    'timelimit' => 2,
330
                    'attempts' => 2,
331
                    'password' => 'test2',
332
                ],
333
                'formdata' => [
334
                    'id' => ':existingid',
335
                    'userid' => null,
336
                    'groupid' => ':groupid',
337
                    'timeopen' => 52,
338
                    'timeclose' => 53,
339
                    'timelimit' => 1,
340
                    'attempts' => 999,
341
                    'password' => 'test',
342
                ],
343
                'expectedrecordscreated' => 0,
344
                'expectedevent' => group_override_updated::class,
345
            ],
346
            'attempts is set to unlimited (i.e. 0)' => [
347
                'existingdata' => [],
348
                'formdata' => [
349
                    'userid' => ':userid',
350
                    'groupid' => null,
351
                    'timeopen' => null,
352
                    'timeclose' => null,
353
                    'timelimit' => null,
354
                    // This checks we are using empty() carefully, since this is valid.
355
                    'attempts' => 0,
356
                    'password' => null,
357
                ],
358
                'expectedrecordscreated' => 1,
359
                'expectedevent' => user_override_created::class,
360
            ],
361
            'some settings submitted are the same as what is in the quiz (valid)' => [
362
                'existingdata' => [],
363
                'formdata' => [
364
                    'userid' => ':userid',
365
                    'groupid' => null,
366
                    // Make these the same, they should be ignored.
367
                    'timeopen' => self::TEST_QUIZ_SETTINGS['timeopen'],
368
                    'timeclose' => self::TEST_QUIZ_SETTINGS['timeclose'],
369
                    'attempts' => self::TEST_QUIZ_SETTINGS['attempts'],
370
                    // However change this, this should still get updated.
371
                    'timelimit' => self::TEST_QUIZ_SETTINGS['timelimit'] + 5,
372
                    'password' => null,
373
                ],
374
                'expectedrecordscreated' => 1,
375
                'expectedevent' => user_override_created::class,
376
            ],
377
        ];
378
    }
379
 
380
    /**
381
     * Tests save_override function
382
     *
383
     * @param array $existingdata If given, an existing override will be created.
384
     * @param array $formdata The data being tested, simulating being submitted
385
     * @param int $expectedrecordscreated The number of records that are expected to be created by upsert
386
     * @param string $expectedeventclass an event class, which is expected to the emitted by upsert
387
     * @dataProvider save_and_get_override_provider
388
     */
389
    public function test_save_and_get_override(
390
        array $existingdata,
391
        array $formdata,
392
        int $expectedrecordscreated,
393
        string $expectedeventclass
394
    ): void {
395
        global $DB;
396
 
397
        $this->setAdminUser();
398
        $this->resetAfterTest();
399
 
400
        $test = $this->setup_existing_and_testing_data($existingdata, $formdata);
401
        $manager = $test->quizobj->get_override_manager();
402
 
403
        // Get the count before.
404
        $beforecount = $DB->count_records('quiz_overrides');
405
 
406
        $sink = $this->redirectEvents();
407
 
408
        // Submit the form data.
409
        $id = $manager->save_override($test->formdata);
410
 
411
        // Get the count after and compare to the expected.
412
        $aftercount = $DB->count_records('quiz_overrides');
413
        $this->assertEquals($expectedrecordscreated, $aftercount - $beforecount);
414
 
415
        // Read back the created/updated value, and compare it to the formdata.
416
        $readback = $DB->get_record('quiz_overrides', ['id' => $id]);
417
        $this->assertNotEmpty($readback);
418
 
419
        foreach ($test->formdata as $key => $value) {
420
            // If the value is the same as the quiz, we expect it to be null.
421
            if (!empty(self::TEST_QUIZ_SETTINGS[$key]) && $value == self::TEST_QUIZ_SETTINGS[$key]) {
422
                $this->assertNull($readback->{$key});
423
            } else {
424
                // Else we expect the value to have been set.
425
                $this->assertEquals($value, $readback->{$key});
426
            }
427
        }
428
 
429
        // Check the get_all_overrides function returns this data as well.
430
        $alloverrideids = array_column($manager->get_all_overrides(), 'id');
431
 
432
        $this->assertCount($aftercount, $alloverrideids);
433
        $this->assertTrue(in_array($id, $alloverrideids));
434
 
435
        // Check that the calendar events are created as well.
436
        // This is only if the times were set, and they were set differently to the default.
437
        $expectedcount = 0;
438
 
439
        if (!empty($formdata['timeopen']) && $formdata['timeopen'] != self::TEST_QUIZ_SETTINGS['timeopen']) {
440
            $expectedcount += 1;
441
        }
442
 
443
        if (!empty($formdata['timeclose']) && $formdata['timeclose'] != self::TEST_QUIZ_SETTINGS['timeclose']) {
444
            $expectedcount += 1;
445
        }
446
 
447
        // Find all events. We assume the test event times do not exceed a time of 999.
448
        $events = calendar_get_events(0, 999, [$test->user1->id], [$test->groupid1], false);
449
        $this->assertCount($expectedcount, $events);
450
 
451
        // Check the expected event was also emitted.
452
        if (!empty($expectedeventclass)) {
453
            $events = $sink->get_events();
454
            $eventclasses = array_map(fn($event) => get_class($event), $events);
455
            $this->assertTrue(in_array($expectedeventclass, $eventclasses));
456
        }
457
    }
458
 
459
    /**
460
     * Tests that when saving an override, validation is performed and an exception is thrown if this fails.
461
     * Note - this does not test every validation scenario, for that {@see validate_data_provider}
462
     */
463
    public function test_save_override_validation_throws_exception(): void {
464
        $this->setAdminUser();
465
        $this->resetAfterTest();
466
 
467
        [$quizobj] = $this->create_quiz_and_course();
468
        $manager = $quizobj->get_override_manager();
469
 
470
        // Submit empty (bad data).
471
        $this->expectException(\invalid_parameter_exception::class);
472
        $this->expectExceptionMessage(get_string('nooverridedata', 'quiz'));
473
        $manager->save_override([]);
474
    }
475
 
476
    /**
477
     * Provides values to test_validate_data
478
     *
479
     * @return array
480
     */
481
    public static function validate_data_provider(): array {
482
        return [
483
            'valid create for user' => [
484
                'existingdata' => [],
485
                'formdata' => [
486
                    'userid' => ':userid',
487
                    'groupid' => null,
488
                    'timeopen' => 50,
489
                    'timeclose' => 51,
490
                    'timelimit' => 2,
491
                    'attempts' => 2,
492
                    'password' => 'test2',
493
                ],
494
                'expectedreturn' => [],
495
            ],
496
            'valid create for group' => [
497
                'existingdata' => [],
498
                'formdata' => [
499
                    'userid' => null,
500
                    'groupid' => ':groupid',
501
                    'timeopen' => 50,
502
                    'timeclose' => 51,
503
                    'timelimit' => 2,
504
                    'attempts' => 2,
505
                    'password' => 'test2',
506
                ],
507
                'expectedreturn' => [],
508
            ],
509
            'valid update for user' => [
510
                'existingdata' => [
511
                    'userid' => ':userid',
512
                    'groupid' => null,
513
                    'timeopen' => 50,
514
                    'timeclose' => 51,
515
                    'timelimit' => 2,
516
                    'attempts' => 2,
517
                    'password' => 'test2',
518
                ],
519
                'formdata' => [
520
                    'id' => ':existingid',
521
                    'userid' => ':userid',
522
                    'groupid' => null,
523
                    'timeopen' => 50,
524
                    'timeclose' => 51,
525
                    'timelimit' => 2,
526
                    'attempts' => 2,
527
                    'password' => 'test2',
528
                ],
529
                'expectedreturn' => [],
530
            ],
531
            'valid update for group' => [
532
                'existingdata' => [
533
                    'userid' => null,
534
                    'groupid' => ':groupid',
535
                    'timeopen' => 50,
536
                    'timeclose' => 51,
537
                    'timelimit' => 2,
538
                    'attempts' => 2,
539
                    'password' => 'test2',
540
                ],
541
                'formdata' => [
542
                    'id' => ':existingid',
543
                    'userid' => null,
544
                    'groupid' => ':groupid',
545
                    'timeopen' => 50,
546
                    'timeclose' => 51,
547
                    'timelimit' => 2,
548
                    'attempts' => 2,
549
                    'password' => 'test2',
550
                ],
551
                'expectedreturn' => [],
552
            ],
553
            'update but without user or group specified in update' => [
554
                'existingdata' => [
555
                    'userid' => ':userid',
556
                    'groupid' => null,
557
                    'timeopen' => 50,
558
                    'timeclose' => 51,
559
                    'timelimit' => 2,
560
                    'attempts' => 2,
561
                    'password' => 'test2',
562
                ],
563
                'formdata' => [
564
                    'id' => ':existingid',
565
                    'userid' => null,
566
                    'groupid' => null,
567
                    'timeopen' => 52,
568
                    'timeclose' => 53,
569
                    'timelimit' => 1,
570
                    'attempts' => 999,
571
                    'password' => 'test',
572
                ],
573
                'expectedreturn' => [
574
                    'general' => get_string('overridemustsetuserorgroup', 'quiz'),
575
                ],
576
            ],
577
            'both userid and groupid specified' => [
578
                'existingdata' => [],
579
                'formdata' => [
580
                    'userid' => ':userid',
581
                    'groupid' => ':groupid',
582
                    'timeopen' => 50,
583
                    'timeclose' => 100,
584
                    'timelimit' => null,
585
                    'attempts' => null,
586
                    'password' => null,
587
                ],
588
                'expectedreturn' => [
589
                    'general' => get_string('overridecannotsetbothgroupanduser', 'quiz'),
590
                ],
591
            ],
592
            'neither userid nor groupid specified' => [
593
                'existingdata' => [],
594
                'formdata' => [
595
                    'userid' => null,
596
                    'groupid' => null,
597
                    'timeopen' => 50,
598
                    'timeclose' => 100,
599
                    'timelimit' => null,
600
                    'attempts' => null,
601
                    'password' => null,
602
                ],
603
                'expectedreturn' => [
604
                    'general' => get_string('overridemustsetuserorgroup', 'quiz'),
605
                ],
606
            ],
607
            'empty data' => [
608
                'existingdata' => [],
609
                'formdata' => [],
610
                'expectedreturn' => [
611
                    'general' => get_string('nooverridedata', 'quiz'),
612
                ],
613
            ],
614
            'all nulls' => [
615
                'existingdata' => [],
616
                'formdata' => [
617
                    'userid' => null,
618
                    'groupid' => null,
619
                    'timeopen' => null,
620
                    'timeclose' => null,
621
                    'timelimit' => null,
622
                    'attempts' => null,
623
                    'password' => null,
624
                ],
625
                'expectedreturn' => [
626
                    'general' => get_string('nooverridedata', 'quiz'),
627
                ],
628
            ],
629
            'user given, rest are nulls' => [
630
                'existingdata' => [],
631
                'formdata' => [
632
                    'userid' => ':userid',
633
                    'groupid' => null,
634
                    'timeopen' => null,
635
                    'timeclose' => null,
636
                    'timelimit' => null,
637
                    'attempts' => null,
638
                    'password' => null,
639
                ],
640
                'expectedreturn' => [
641
                    'general' => get_string('nooverridedata', 'quiz'),
642
                ],
643
            ],
644
            'all submitted data was the same as the existing quiz' => [
645
                'existingdata' => [],
646
                'formdata' => [
647
                    'userid' => ':userid',
648
                    'groupid' => null,
649
                    'timeopen' => self::TEST_QUIZ_SETTINGS['timeopen'],
650
                    'timeclose' => self::TEST_QUIZ_SETTINGS['timeclose'],
651
                    'attempts' => self::TEST_QUIZ_SETTINGS['attempts'],
652
                    'timelimit' => self::TEST_QUIZ_SETTINGS['timelimit'],
653
                    'password' => null,
654
                ],
655
                'expectedreturn' => [
656
                    'general' => get_string('nooverridedata', 'quiz'),
657
                ],
658
            ],
659
            'userid is invalid' => [
660
                'existingdata' => [],
661
                'formdata' => [
662
                    'userid' => -1,
663
                    'groupid' => null,
664
                    'timeopen' => null,
665
                    'timeclose' => null,
666
                    'timelimit' => null,
667
                    'attempts' => null,
668
                    'password' => 'mypass',
669
                ],
670
                'expectedreturn' => [
671
                    'userid' => get_string('overrideinvaliduser', 'quiz'),
672
                ],
673
            ],
674
            'groupid is invalid' => [
675
                'existingdata' => [],
676
                'formdata' => [
677
                    'userid' => null,
678
                    'groupid' => -1,
679
                    'timeopen' => null,
680
                    'timeclose' => null,
681
                    'timelimit' => null,
682
                    'attempts' => null,
683
                    'password' => 'mypass',
684
                ],
685
                'expectedreturn' => [
686
                    'groupid' => get_string('overrideinvalidgroup', 'quiz'),
687
                ],
688
            ],
689
            'timeclose is before timeopen' => [
690
                'existingdata' => [],
691
                'formdata' => [
692
                    'userid' => ':userid',
693
                    'groupid' => null,
694
                    'timeopen' => 50,
695
                    'timeclose' => 10,
696
                    'timelimit' => null,
697
                    'attempts' => null,
698
                    'password' => null,
699
                ],
700
                'expectedreturn' => [
701
                    'timeclose' => get_string('closebeforeopen', 'quiz'),
702
                ],
703
            ],
704
            'timeclose is same as timeopen' => [
705
                'existingdata' => [],
706
                'formdata' => [
707
                    'userid' => ':userid',
708
                    'groupid' => null,
709
                    'timeopen' => 50,
710
                    'timeclose' => 50,
711
                    'timelimit' => null,
712
                    'attempts' => null,
713
                    'password' => null,
714
                ],
715
                'expectedreturn' => [
716
                    'timeclose' => get_string('closebeforeopen', 'quiz'),
717
                ],
718
            ],
719
            'timelimit is negative' => [
720
                'existingdata' => [],
721
                'formdata' => [
722
                    'userid' => ':userid',
723
                    'groupid' => null,
724
                    'timeopen' => null,
725
                    'timeclose' => null,
726
                    'timelimit' => -1,
727
                    'attempts' => null,
728
                    'password' => null,
729
                ],
730
                'expectedreturn' => [
731
                    'timelimit' => get_string('overrideinvalidtimelimit', 'quiz'),
732
                ],
733
            ],
734
            'attempts is negative' => [
735
                'existingdata' => [],
736
                'formdata' => [
737
                    'userid' => ':userid',
738
                    'groupid' => null,
739
                    'timeopen' => null,
740
                    'timeclose' => null,
741
                    'timelimit' => null,
742
                    'attempts' => -1,
743
                    'password' => null,
744
                ],
745
                'expectedreturn' => [
746
                    'attempts' => get_string('overrideinvalidattempts', 'quiz'),
747
                ],
748
            ],
749
            'existing id given to update is invalid' => [
750
                'existingdata' => [],
751
                'formdata' => [
752
                    'id' => 999999999999,
753
                    'userid' => ':userid',
754
                    'groupid' => null,
755
                    'timeopen' => 50,
756
                    'timeclose' => 51,
757
                    'timelimit' => null,
758
                    'attempts' => null,
759
                    'password' => null,
760
                ],
761
                'expectedreturn' => [
762
                    'general' => get_string('overrideinvalidexistingid', 'quiz'),
763
                ],
764
            ],
765
            'userid changed after creation' => [
766
                'existingdata' => [
767
                    'userid' => ":userid",
768
                    'groupid' => null,
769
                    'timeopen' => 50,
770
                    'timeclose' => 51,
771
                    'timelimit' => 2,
772
                    'attempts' => 2,
773
                    'password' => 'test2',
774
                ],
775
                'formdata' => [
776
                    'id' => ':existingid',
777
                    'userid' => ":user2id",
778
                    'groupid' => null,
779
                    'timeopen' => 50,
780
                    'timeclose' => 51,
781
                    'timelimit' => null,
782
                    'attempts' => null,
783
                    'password' => null,
784
                ],
785
                'expectedreturn' => [
786
                   'userid' => get_string('overridecannotchange', 'quiz'),
787
                ],
788
            ],
789
            'groupid changed after creation' => [
790
                'existingdata' => [
791
                    'userid' => null,
792
                    'groupid' => ':groupid',
793
                    'timeopen' => 50,
794
                    'timeclose' => 51,
795
                    'timelimit' => 2,
796
                    'attempts' => 2,
797
                    'password' => 'test2',
798
                ],
799
                'formdata' => [
800
                    'id' => ':existingid',
801
                    'userid' => null,
802
                    'groupid' => ':group2id',
803
                    'timeopen' => 50,
804
                    'timeclose' => 51,
805
                    'timelimit' => null,
806
                    'attempts' => null,
807
                    'password' => null,
808
                ],
809
                'expectedreturn' => [
810
                   'groupid' => get_string('overridecannotchange', 'quiz'),
811
                ],
812
            ],
813
            'create multiple for the same user' => [
814
                'existingdata' => [
815
                    'userid' => ':userid',
816
                    'groupid' => null,
817
                    'timeopen' => 50,
818
                    'timeclose' => 51,
819
                    'timelimit' => 2,
820
                    'attempts' => 2,
821
                    'password' => 'test2',
822
                ],
823
                'formdata' => [
824
                    'userid' => ':userid',
825
                    'groupid' => null,
826
                    'timeopen' => 50,
827
                    'timeclose' => 51,
828
                    'timelimit' => 2,
829
                    'attempts' => 2,
830
                    'password' => 'test2',
831
                ],
832
                'expectedreturn' => [
833
                    'general' => get_string('overridemultiplerecordsexist', 'quiz'),
834
                ],
835
            ],
836
            'create multiple for the same group' => [
837
                'existingdata' => [
838
                    'userid' => null,
839
                    'groupid' => ':groupid',
840
                    'timeopen' => 50,
841
                    'timeclose' => 51,
842
                    'timelimit' => 2,
843
                    'attempts' => 2,
844
                    'password' => 'test2',
845
                ],
846
                'formdata' => [
847
                    'userid' => null,
848
                    'groupid' => ':groupid',
849
                    'timeopen' => 50,
850
                    'timeclose' => 51,
851
                    'timelimit' => 2,
852
                    'attempts' => 2,
853
                    'password' => 'test2',
854
                ],
855
                'expectedreturn' => [
856
                    'general' => get_string('overridemultiplerecordsexist', 'quiz'),
857
                ],
858
            ],
859
        ];
860
    }
861
 
862
    /**
863
     * Tests validate_data function
864
     *
865
     * @param array $existingdata If given, an existing override will be created.
866
     * @param array $formdata The data being tested, simulating being submitted
867
     * @param array $expectedreturn expected keys and associated values expected to be returned from validate_data
868
     * @dataProvider validate_data_provider
869
     */
870
    public function test_validate_data(array $existingdata, array $formdata, array $expectedreturn): void {
871
        $this->setAdminUser();
872
        $this->resetAfterTest();
873
 
874
        // Setup the test.
875
        $test = $this->setup_existing_and_testing_data($existingdata, $formdata);
876
 
877
        // Validate.
878
        $manager = $test->quizobj->get_override_manager();
879
        $result = $manager->validate_data($test->formdata);
880
 
881
        // Ensure all expected errors appear in the return.
882
        foreach ($expectedreturn as $key => $error) {
883
            // Ensure it is set.
884
            $this->assertContains($key, array_keys($result));
885
 
886
            // Ensure the message contains the expected error.
887
            $this->assertStringContainsString($error, $result[$key]);
888
        }
889
 
890
        // Ensure there are no extra returned errors than what was expected.
891
        $extra = array_diff_key($result, $expectedreturn);
892
        $this->assertEmpty($extra, 'More validation errors were returned than expected');
893
    }
894
 
895
    /**
896
     * Provide delete functions to test
897
     *
898
     * @return array
899
     */
900
    public static function delete_override_provider(): array {
901
        return [
902
            'delete by id (no events logged)' => [
903
                'function' => fn($manager, $override) => $manager->delete_overrides_by_id([$override->id], false, false),
904
                'checkeventslogged' => false,
905
            ],
906
            'delete single (no events logged)' => [
907
                'function' => fn($manager, $override) => $manager->delete_overrides([$override], false, false),
908
                'checkeventslogged' => false,
909
            ],
910
            'delete all (no events logged)' => [
911
                'function' => fn($manager, $override) => $manager->delete_all_overrides(false, false),
912
                'checkeventslogged' => false,
913
            ],
914
            'delete by id (events logged)' => [
915
                'function' => fn($manager, $override) => $manager->delete_overrides_by_id([$override->id], true, false),
916
                'checkeventslogged' => true,
917
            ],
918
            'delete single (events logged)' => [
919
                'function' => fn($manager, $override) => $manager->delete_overrides([$override], true, false),
920
                'checkeventslogged' => true,
921
            ],
922
            'delete all (events logged)' => [
923
                'function' => fn($manager, $override) => $manager->delete_all_overrides(true, false),
924
                'checkeventslogged' => true,
925
            ],
926
            'delete all in database (events logged)' => [
927
                'function' => fn($manager, $override) => $manager->delete_all_overrides(true, false),
928
                'checkeventslogged' => true,
929
            ],
930
        ];
931
    }
932
 
933
    /**
934
     * Tests deleting override functions
935
     *
936
     * @param \Closure $deletefunction delete function to be called.
937
     * @param bool $checkeventslogged if true, will check that events were logged.
938
     * @dataProvider delete_override_provider
939
     */
940
    public function test_delete_override(\Closure $deletefunction, bool $checkeventslogged): void {
941
        $this->setAdminUser();
942
        $this->resetAfterTest();
943
        [$quizobj] = $this->create_quiz_and_course();
944
        $user = $this->getDataGenerator()->create_user();
945
 
946
        // Create an override.
947
        $data = [
948
            'userid' => $user->id,
949
            'timeopen' => 500,
950
        ];
951
        $manager = $quizobj->get_override_manager();
952
        $id = $manager->save_override($data);
953
 
954
        // Check the calendar event was made.
955
        $this->assertCount(1, calendar_get_events(0, 999, [$user->id], false, false));
956
 
957
        // Check that the cache was made.
958
        $overridecache = new override_cache($quizobj->get_quizid());
959
        $this->assertNotEmpty($overridecache->get_cached_user_override($user->id));
960
 
961
        // Capture events.
962
        $sink = $this->redirectEvents();
963
 
964
        $override = (object) [
965
            'id' => $id,
966
            'userid' => $user->id,
967
        ];
968
 
969
        // Delete the override.
970
        $deletefunction($manager, $override);
971
 
972
        // Check the calendar event was deleted.
973
        $this->assertCount(0, calendar_get_events(0, 999, [$user->id], false, false));
974
 
975
        // Check that the cache was cleared.
976
        $this->assertEmpty($overridecache->get_cached_user_override($user->id));
977
 
978
        // Check the event was logged.
979
        if ($checkeventslogged) {
980
            $events = $sink->get_events();
981
            $eventclasses = array_map(fn($e) => get_class($e), $events);
982
            $this->assertTrue(in_array(user_override_deleted::class, $eventclasses));
983
        }
984
    }
985
 
986
    /**
987
     * Creates a role with the given capabilities and assigns it to the user.
988
     *
989
     * @param int $userid user to assign role to
990
     * @param array $capabilities array of $capname => $permission to add to role
991
     */
992
    private function give_user_role_with_capabilities(int $userid, array $capabilities): void {
993
        // Setup the role and permissions.
994
        $roleid = $this->getDataGenerator()->create_role();
995
        foreach ($capabilities as $capname => $permission) {
996
            role_change_permission($roleid, \context_system::instance(), $capname, $permission);
997
        }
998
 
999
        $user = $this->getDataGenerator()->create_user();
1000
        role_assign($roleid, $userid, \context_system::instance()->id);
1001
    }
1002
 
1003
    /**
1004
     * Provides values to test_require_read_capability
1005
     *
1006
     * @return array
1007
     */
1008
    public static function require_read_capability_provider(): array {
1009
        $readfunc = fn($manager) => $manager->require_read_capability();
1010
        $managefunc = fn($manager) => $manager->require_manage_capability();
1011
 
1012
        return [
1013
            'reading - cannot read' => [
1014
                'capabilitiestogive' => [],
1015
                'expectedallowed' => false,
1016
                'functionbeingtested' => $readfunc,
1017
            ],
1018
            'reading - can read' => [
1019
                'capabilitiestogive' => ['mod/quiz:viewoverrides' => CAP_ALLOW],
1020
                'expectedallowed' => true,
1021
                'functionbeingtested' => $readfunc,
1022
            ],
1023
            'reading - can manage (so can also read)' => [
1024
                'capabilitiestogive' => ['mod/quiz:manageoverrides' => CAP_ALLOW],
1025
                'expectedallowed' => true,
1026
                'functionbeingtested' => $readfunc,
1027
            ],
1028
            'manage - cannot manage' => [
1029
                'capabilitiestogive' => [],
1030
                'expectedallowed' => false,
1031
                'functionbeingtested' => $managefunc,
1032
            ],
1033
            'manage - can only read' => [
1034
                'capabilitiestogive' => ['mod/quiz:viewoverrides' => CAP_ALLOW],
1035
                'expectedallowed' => false,
1036
                'functionbeingtested' => $managefunc,
1037
            ],
1038
            'manage - can manage' => [
1039
                'capabilitiestogive' => ['mod/quiz:manageoverrides' => CAP_ALLOW],
1040
                'expectedallowed' => true,
1041
                'functionbeingtested' => $managefunc,
1042
            ],
1043
        ];
1044
    }
1045
 
1046
    /**
1047
     * Tests require_read_capability
1048
     *
1049
     * @param array $capabilitiestogive array of capability => value to give to test user
1050
     * @param bool $expectedallowed if false, will expect required_capability_exception to be thrown
1051
     * @param \Closure $functionbeingtested is passed the manager and calls the function being tested (usually require_*_capability)
1052
     * @dataProvider require_read_capability_provider
1053
     */
1054
    public function test_require_read_capability(
1055
        array $capabilitiestogive,
1056
        bool $expectedallowed,
1057
        \Closure $functionbeingtested
1058
    ): void {
1059
        $this->resetAfterTest();
1060
        [$quizobj] = $this->create_quiz_and_course();
1061
        $user = $this->getDataGenerator()->create_user();
1062
        $this->give_user_role_with_capabilities($user->id, $capabilitiestogive);
1063
        $this->setUser($user);
1064
 
1065
        if (!$expectedallowed) {
1066
            $this->expectException(\required_capability_exception::class);
1067
        }
1068
        $functionbeingtested($quizobj->get_override_manager());
1069
    }
1070
 
1071
    /**
1072
     * Tests delete_orphaned_group_overrides_in_course
1073
     */
1074
    public function test_delete_orphaned_group_overrides_in_course(): void {
1075
        global $DB;
1076
 
1077
        $this->setAdminUser();
1078
        $this->resetAfterTest();
1079
        [$quizobj, $course] = $this->create_quiz_and_course();
1080
 
1081
        // Create a two group and one user overrides.
1082
        $groupid = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test']);
1083
        $groupdata = [
1084
            'quiz' => $quizobj->get_quizid(),
1085
            'groupid' => $groupid,
1086
            'password' => 'test',
1087
        ];
1088
 
1089
        $group2id = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test2']);
1090
        $group2data = [
1091
            'quiz' => $quizobj->get_quizid(),
1092
            'groupid' => $group2id,
1093
            'password' => 'test',
1094
        ];
1095
 
1096
        $userid = $this->getDataGenerator()->create_user()->id;
1097
        $userdata = [
1098
            'quiz' => $quizobj->get_quizid(),
1099
            'userid' => $userid,
1100
            'password' => 'test',
1101
        ];
1102
 
1103
        $manager = $quizobj->get_override_manager();
1104
        $manager->save_override($groupdata);
1105
        $useroverrideid = $manager->save_override($userdata);
1106
        $group2overrideid = $manager->save_override($group2data);
1107
 
1108
        $this->assertCount(3, $manager->get_all_overrides());
1109
 
1110
        // Delete the first group (via the DB, so that the callbacks are not run).
1111
        $DB->delete_records('groups', ['id' => $groupid]);
1112
 
1113
        // Confirm the overrides still exist (no callback has been run yet).
1114
        $this->assertCount(3, $manager->get_all_overrides());
1115
 
1116
        // Run orphaned record callback.
1117
        override_manager::delete_orphaned_group_overrides_in_course($course->id);
1118
 
1119
        // Confirm it has now been deleted (but user and other group override still exists).
1120
        $overrides = $manager->get_all_overrides();
1121
        $this->assertCount(2, $overrides);
1122
        $this->assertArrayHasKey($useroverrideid, $overrides);
1123
        $this->assertArrayHasKey($group2overrideid, $overrides);
1124
    }
1125
 
1126
    /**
1127
     * Tests deleting by id but providing an invalid id
1128
     */
1129
    public function test_delete_by_id_invalid_id(): void {
1130
        $this->setAdminUser();
1131
        $this->resetAfterTest();
1132
        [$quizobj] = $this->create_quiz_and_course();
1133
 
1134
        $this->expectException(\invalid_parameter_exception::class);
1135
        $this->expectExceptionMessage(get_string('overridemissingdelete', 'quiz', '0,1'));
1136
 
1137
        // These ids do not exist, so this should throw an error.
1138
        $quizobj->get_override_manager()->delete_overrides_by_id([0, 1]);
1139
    }
1140
 
1141
    /**
1142
     * Tests that constructing a override manager with mismatching quiz and context throws an exception
1143
     */
1144
    public function test_quiz_context_mismatch(): void {
1145
        $this->resetAfterTest();
1146
 
1147
        // Create one quiz for context, but make the quiz given have an incorrect cmid.
1148
        [$quizobj] = $this->create_quiz_and_course();
1149
        $context = \context_module::instance($quizobj->get_cmid());
1150
 
1151
        $quiz = (object)[
1152
            'cmid' => $context->instanceid + 1,
1153
        ];
1154
 
1155
        $this->expectException(\coding_exception::class);
1156
        $this->expectExceptionMessage("Given context does not match the quiz object");
1157
        new override_manager($quiz, $context);
1158
    }
1159
}